The Coline App SDK: apps, tools, file types and surfaces that run inside Coline workspaces, and public websites with Coline Sites. TypeScript, with an in-memory test workspace. MIT.
| * |
| 6 | * Collections are still created on first use. Declaring one is only needed |
| 7 | * to open it to site visitors: `access` says which visitors may read, |
| 8 | * create, update and delete its records. A collection that is not declared, |
| 9 | * or declared without `access`, is invisible to visitors. |
| 10 | * |
| 11 | * Members and the app's own logic never read these rules. They keep the |
| 12 | * per-record access model (`ColineRecordAccess`). |
| 13 | * |
| 14 | * Subjects: |
| 15 | * - `public` anyone who opens the site |
| 16 | * - `visitor` any guest or signed-in visitor (the same as `public` for |
| 17 | * writes, because a write always creates a guest session) |
| 18 | * - `signedIn` visitors who signed in with Coline |
| 19 | * - `owner` the visitor who created the record |
| 20 | * - `team` nobody on the site; workspace members and app logic only |
| 21 | */ |
| 22 |
| 23 | export const COLLECTION_ACCESS_SUBJECTS = ["public", "visitor", "signedIn", "owner", "team"] as const; |
| 24 | export const collectionAccessSubjectSchema = z.enum(COLLECTION_ACCESS_SUBJECTS); |
| 25 | export type CollectionAccessSubject = z.infer<typeof collectionAccessSubjectSchema>; |
| 26 |
| 27 | export const collectionAccessSchema = z |
| 28 | .strictObject({ |
| 29 | read: collectionAccessSubjectSchema.default("team"), |
| 30 | create: collectionAccessSubjectSchema.default("team"), |
| 31 | update: collectionAccessSubjectSchema.default("team"), |
| 32 | delete: collectionAccessSubjectSchema.default("team"), |
| 33 | }) |
| 34 | .superRefine((access, ctx) => { |
| 35 | if (access.create === "owner") { |
| 36 | ctx.addIssue({ |
| 37 | code: "custom", |
| 38 | path: ["create"], |
| 39 | message: 'A record has no owner before it exists. Use "visitor" or "signedIn" for create.', |
| 40 | }); |
| 41 | } |
| 42 | }); |
| 43 | export type CollectionAccess = z.infer<typeof collectionAccessSchema>; |
| 44 | export type CollectionAccessInput = z.input<typeof collectionAccessSchema>; |
| 45 |
| 46 | export const appCollectionNameSchema = z |
| 47 | .string() |
| 48 | .trim() |
| 49 | .regex(/^[a-z][a-z0-9_-]*$/, "Collection names are lowercase identifiers.") |
| 50 | .max(120); |
| 51 |
| 52 | export const appCollectionDeclarationSchema = z.strictObject({ |
| 53 | description: z.string().trim().max(500).optional(), |
| 54 | access: collectionAccessSchema.optional(), |
| 55 | /** |
| 56 | * JSON Schema for each record's data, generated from the collection's zod |
| 57 | * `schema` in app.config.ts. Coline checks every visitor write against |
| 58 | * it; the app's own writes are checked by TypeScript. |
| 59 | */ |
| 60 | schema: z |
| 61 | .record(z.string(), z.unknown()) |
| 62 | .refine((schema) => JSON.stringify(schema).length <= 20_000, "Collection schemas are limited to 20 kB.") |
| 63 | .optional(), |
| 64 | }); |
| 65 | export type AppCollectionDeclaration = z.infer<typeof appCollectionDeclarationSchema>; |
| 66 |
| 67 | export const appCollectionsSchema = z |
| 68 | .record(appCollectionNameSchema, appCollectionDeclarationSchema) |
| 69 | .default({}) |
| 70 | .refine((collections) => Object.keys(collections).length <= 50, "Declare at most 50 collections."); |
| 71 | export type AppCollections = z.infer<typeof appCollectionsSchema>; |
| 72 |
| 73 | export type CollectionAction = "read" | "create" | "update" | "delete"; |
| 74 |
| 75 | /** The visitor rule for one action on one collection; `team` when undeclared. */ |
| 76 | export function collectionRuleFor( |
| 77 | collections: AppCollections, |
| 78 | collection: string, |
| 79 | action: CollectionAction, |
| 80 | ): CollectionAccessSubject { |
| 81 | return collections[collection]?.access?.[action] ?? "team"; |
| 82 | } |
| 83 |
| 84 | /** Collections that any visitor action can reach, for consent and review copy. */ |
| 85 | export function visitorReachableCollections(collections: AppCollections): string[] { |
| 86 | return Object.entries(collections) |
| 87 | .filter(([, declaration]) => { |
| 88 | const access = declaration.access; |
| 89 | if (!access) return false; |
| 90 | return [access.read, access.create, access.update, access.delete].some((subject) => subject !== "team"); |
| 91 | }) |
| 92 | .map(([name]) => name) |
| 93 | .sort(); |
| 94 | } |
| 95 |