// Organizations, like on GitHub: accounts that own repositories for a group of people. // // An organization is a record in `users` with kind "org", so it shares the namespace of usernames // and repositories keep pointing at their owner the same way (//). Nobody signs in as // one: it has no email and a random password, and the auth rule refuses it anyway. // // `org_members` gives people a role in it, which is their access to all of its repositories: // read, write, admin (also creates, configures and deletes repositories) or owner (also manages // the organization and its members). People join by accepting an invitation. const ROLES = ["owner", "admin", "write", "read"]; const number = (name, opts = {}) => ({ type: "number", name, onlyInt: true, ...opts }); const time = (name, opts = {}) => number(name, { min: 0, ...opts }); const select = (name, values, opts = {}) => ({ type: "select", name, values, maxSelect: 1, required: true, ...opts }); const relation = (name, collection, opts = {}) => ({ type: "relation", name, collectionId: collection.id, maxSelect: 1, cascadeDelete: false, ...opts, }); const parent = (name, collection) => relation(name, collection, { required: true, cascadeDelete: true }); function create(app, name, fields, indexes = []) { const collection = new Collection({ type: "base", name, listRule: null, viewRule: null, createRule: null, updateRule: null, deleteRule: null, fields, indexes, }); app.save(collection); return collection; } migrate( (app) => { const users = app.findCollectionByNameOrId("users"); // "" for a person. users.fields.add(new SelectField({ name: "kind", values: ["org"], maxSelect: 1 })); // Organizations have none; the unique index on email skips empty ones. users.fields.getByName("email").required = false; users.authRule = "kind != 'org'"; app.save(users); create( app, "org_members", [parent("org", users), parent("user", users), select("role", ROLES), time("created_at", { required: true })], ["CREATE UNIQUE INDEX idx_org_members ON org_members (org, user)", "CREATE INDEX idx_org_members_user ON org_members (user)"], ); // Pending until the person invited accepts (and becomes a member) or declines. create( app, "org_invitations", [ parent("org", users), parent("user", users), select("role", ROLES), relation("invited_by", users, { cascadeDelete: true }), time("created_at", { required: true }), ], ["CREATE UNIQUE INDEX idx_org_invitations ON org_invitations (org, user)", "CREATE INDEX idx_org_invitations_user ON org_invitations (user)"], ); }, (app) => { for (const name of ["org_invitations", "org_members"]) app.delete(app.findCollectionByNameOrId(name)); // Organizations can't be people: they go, and their repositories with them. for (const org of app.findRecordsByFilter("users", "kind = 'org'")) app.delete(org); const users = app.findCollectionByNameOrId("users"); users.fields.removeByName("kind"); users.fields.getByName("email").required = true; users.authRule = ""; app.save(users); }, );