/// // // M12 — Create technicianInvites. // // Advisors create invite records and copy the generated invite link. The // frontend stores only a SHA-256 hash of the one-time token; the raw token only // exists in the copied URL. migrate( (db) => { let existing = null; try { existing = db.findCollectionByNameOrId('technicianInvites'); } catch { existing = null; } if (existing) { console.log('[M12] technicianInvites already exists — skipping'); return; } const col = new Collection(); col.name = 'technicianInvites'; col.type = 'base'; col.system = false; col.listRule = 'shopUserId = @request.auth.id || (tokenHash = @request.query.tokenHash && status = "pending" && expiresAt >= @now)'; col.viewRule = 'shopUserId = @request.auth.id || (tokenHash = @request.query.tokenHash && status = "pending" && expiresAt >= @now)'; col.createRule = 'shopUserId = @request.auth.id'; col.updateRule = 'shopUserId = @request.auth.id || (tokenHash = @request.query.tokenHash && status = "pending" && expiresAt >= @now)'; col.deleteRule = 'shopUserId = @request.auth.id'; col.fields.add(new TextField({ name: 'shopUserId', required: true })); col.fields.add(new EmailField({ name: 'email', required: true })); col.fields.add(new TextField({ name: 'displayName', required: false })); col.fields.add(new TextField({ name: 'tokenHash', required: true })); col.fields.add(new SelectField({ name: 'status', required: true, values: ['pending', 'accepted', 'revoked'], maxSelect: 1, })); col.fields.add(new DateField({ name: 'expiresAt', required: true })); col.fields.add(new TextField({ name: 'acceptedByUserId', required: false })); col.fields.add(new DateField({ name: 'acceptedAt', required: false })); db.save(col); console.log('[M12] created technicianInvites collection'); }, (db) => { let col = null; try { col = db.findCollectionByNameOrId('technicianInvites'); } catch { col = null; } if (col) { db.deleteCollection(col); console.log('[M12] deleted technicianInvites collection'); } } );