@phis/contracts 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What Core offers, named once so both sides can spell it the same way.
3
+ *
4
+ * An Add-on declares which Core capabilities it needs and which Core-owned service kinds it
5
+ * implements. Those names are an agreement between two artifacts that ship separately, which is
6
+ * exactly what this package is for -- and until they lived here, an Add-on author had to read them out
7
+ * of phi-server, a private application they never receive. The S3 Storage Add-on carried the media
8
+ * storage digest as a copied hex literal for that reason.
9
+ *
10
+ * This list is a promise, not a plan. A capability appears here when Core delivers it, never before:
11
+ * an Add-on that finds a name here must be able to rely on it. `images:v1` is therefore absent.
12
+ *
13
+ * Still no node builtins. The digests are stated rather than derived, so a manifest stays readable
14
+ * without hashing anything -- Core recomputes them and checks, which is where that belongs.
15
+ */
16
+ /** The Core capabilities an Add-on may require. */
17
+ export const PHI_SERVER_CORE_CAPABILITIES = {
18
+ authentication: "@phis/server/authentication:v1",
19
+ /**
20
+ * "May this actor", in all three of its sources: does the row belong to them, what level do they
21
+ * hold in its group, and what may they do on this Site at all. It is spelled out rather than
22
+ * shortened to `auth` because the shortening would sit one letter away from `authentication` and
23
+ * mean the opposite half of the same word.
24
+ */
25
+ authorization: "@phis/server/authorization:v1",
26
+ /** Running the queries the manifest declared against this Add-on's own tables. */
27
+ data: "@phis/server/data:v1",
28
+ threads: "@phis/server/threads:v1",
29
+ resourceLinks: "@phis/server/resource-links:v1",
30
+ support: "@phis/server/support:v1",
31
+ /** Group facts -- which groups exist, who is in them. What follows from them is `authorization`. */
32
+ groups: "@phis/server/groups:v1",
33
+ storage: "@phis/server/storage:v1",
34
+ /** Reading the secrets an operator set for this Add-on, and no others. */
35
+ secrets: "@phis/server/secrets:v1",
36
+ /** Reading what an operator configured for this Add-on on this Site. */
37
+ settings: "@phis/server/settings:v1",
38
+ /** Whether the acting user holds one of this Add-on's own declared roles. */
39
+ roles: "@phis/server/roles:v1",
40
+ };
41
+ /**
42
+ * The Core-owned service kinds an Add-on may implement.
43
+ *
44
+ * A service kind is not a capability. A capability is something Core offers an Add-on; a service kind
45
+ * is something Core already owns and an Add-on supplies an implementation of -- S3 for Media Storage,
46
+ * LDAP for a Directory. Core keeps selecting the configured implementation itself.
47
+ */
48
+ export const PHI_SERVER_SERVICE_KINDS = {
49
+ mediaStorage: "@phis/server/service/media-storage",
50
+ directory: "@phis/server/service/directory",
51
+ };
52
+ /**
53
+ * The digest of the interface each service kind currently has.
54
+ *
55
+ * An Add-on puts the digest of the interface it was built against into its manifest. Core compares it
56
+ * with what this release offers and refuses the Add-on when they differ -- at install, with the value
57
+ * quoted back, rather than at the first call. Whoever changes the shape of an interface raises its
58
+ * version and replaces the digest here, and every Add-on built against the old one is refused.
59
+ *
60
+ * The digest is `sha256("phi-server-service-kind:<kind>:v<interface version>")`. That recipe is
61
+ * documented for whoever has to produce the next value; nothing needs to run it to read a manifest.
62
+ */
63
+ export const PHI_SERVER_SERVICE_INTERFACE_VERSIONS = {
64
+ [PHI_SERVER_SERVICE_KINDS.mediaStorage]: 1,
65
+ [PHI_SERVER_SERVICE_KINDS.directory]: 1,
66
+ };
67
+ export const PHI_SERVER_SERVICE_INTERFACE_DIGESTS = {
68
+ [PHI_SERVER_SERVICE_KINDS.mediaStorage]: "45a871522511f7d77841ca8dd0113b423b3cfd4cb921f143cd1d3d116cab6952",
69
+ [PHI_SERVER_SERVICE_KINDS.directory]: "2cac7397c7beb1b8e71fc67f5cae3be89182dddf096895570a15deafa1adbfc9",
70
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./addon.js";
2
+ export * from "./capabilities.js";
3
+ export * from "./core.js";
4
+ export * from "./media-storage.js";
5
+ export * from "./queries.js";
6
+ export * from "./schema.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./addon.js";
2
+ export * from "./capabilities.js";
3
+ export * from "./core.js";
4
+ export * from "./media-storage.js";
5
+ export * from "./queries.js";
6
+ export * from "./schema.js";
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The Media Storage service kind, as a Provider implements it.
3
+ *
4
+ * Core owns the upload lifecycle -- `init` and `finalize` stay Core routes for every Provider -- and a
5
+ * Provider states how a body is to be delivered and settles the staged object once it lands. Nothing
6
+ * here reaches a database, a request, or a secret store: the Provider gets configuration and a context,
7
+ * and answers about objects.
8
+ */
9
+ export declare const PHI_LOCAL_MEDIA_STORAGE_PROVIDER_ID: "@phis/server/storage-local";
10
+ export type PhisMediaStorageProviderId = `@${string}/${string}`;
11
+ export type PhisMediaStorageConfig = {
12
+ storageKeyPrefix: string;
13
+ rootDir: string;
14
+ };
15
+ export type PhisMediaObjectInput = {
16
+ storageKey: string;
17
+ body: Buffer | Uint8Array;
18
+ contentType: string;
19
+ metadata?: Record<string, string>;
20
+ };
21
+ export type PhisMediaObjectStreamInput = {
22
+ storageKey: string;
23
+ body: ReadableStream<Uint8Array>;
24
+ contentType: string;
25
+ metadata?: Record<string, string>;
26
+ };
27
+ export type PhisMediaObjectHead = {
28
+ storageKey: string;
29
+ byteSize: number;
30
+ contentType: string;
31
+ etag: string;
32
+ metadata: Record<string, string>;
33
+ lastModifiedAt: string;
34
+ };
35
+ /**
36
+ * How a Client is to deliver one upload body.
37
+ *
38
+ * This is the Provider-neutral half of the upload contract: init answers with a plan, and a generic
39
+ * executor carries it out without knowing which Provider issued it. Before the plan existed the rule
40
+ * "one PUT, to our own route, with the file's content type" lived only in the Client, which is exactly
41
+ * what a Provider that takes the body directly cannot satisfy.
42
+ *
43
+ * The union is meant to grow. A Provider that needs several requests for one body -- S3 multipart --
44
+ * adds a variant here rather than a second upload lifecycle, and the executor refuses a `kind` it does
45
+ * not know instead of falling back to a PUT that would go to the wrong place. What a Provider needs
46
+ * reported back once the body has landed travels through `completion`, opaque to Core.
47
+ */
48
+ export type PhiMediaUploadPlan = {
49
+ kind: "proxy-stream";
50
+ /** A Core route on this origin: the body streams through the Server, with the session cookie. */
51
+ url: string;
52
+ method: "PUT";
53
+ headers?: Record<string, string>;
54
+ } | {
55
+ kind: "presigned-put";
56
+ /** The Provider's own endpoint. Site credentials must never be attached to it. */
57
+ url: string;
58
+ method: "PUT";
59
+ headers: Record<string, string>;
60
+ expiresAt: string;
61
+ };
62
+ export type PhiMediaUploadPlanKindKey = PhiMediaUploadPlan["kind"];
63
+ export type PhiMediaUploadPlanInput = {
64
+ token: string;
65
+ storageKey: string;
66
+ contentType: string;
67
+ sizeBytes: number;
68
+ expiresAt: string;
69
+ /**
70
+ * Where Core itself receives a body, for a Provider that cannot take one directly.
71
+ *
72
+ * Core owns its route table, so the address is handed to the adapter rather than assembled by it.
73
+ */
74
+ proxyUploadUrl: string;
75
+ };
76
+ export type PhiMediaUploadCompletionInput = {
77
+ storageKey: string;
78
+ /** Whatever the plan's issuer asked the Client to bring back. Core passes it through unread. */
79
+ completion?: unknown;
80
+ };
81
+ export interface PhisMediaStorageAdapter {
82
+ putObject(input: PhisMediaObjectInput): Promise<PhisMediaObjectHead>;
83
+ putObjectStream(input: PhisMediaObjectStreamInput): Promise<PhisMediaObjectHead>;
84
+ getObject(storageKey: string): Promise<Buffer | null>;
85
+ /**
86
+ * The first bytes of an object, for deciding what it actually is without moving it.
87
+ *
88
+ * Separate from `getObject` because the answer needs a few kilobytes and an object may be gigabytes:
89
+ * a Provider that stores remotely reads a range rather than the body, so identifying an upload costs
90
+ * one small request whatever was uploaded.
91
+ */
92
+ readObjectHead(storageKey: string, byteLength: number): Promise<Buffer | null>;
93
+ /**
94
+ * Settles a staged object onto its final key without moving it through Core.
95
+ *
96
+ * `finalize` used to read the whole staged body and write it back, which is a download plus an upload
97
+ * of every byte for a Provider that stores remotely -- impossible for a multipart object and wasteful
98
+ * for anything large. A Provider that can copy in place does; the Local adapter copies on disk.
99
+ */
100
+ copyObject(input: {
101
+ sourceKey: string;
102
+ targetKey: string;
103
+ contentType: string;
104
+ metadata?: Record<string, string>;
105
+ }): Promise<PhisMediaObjectHead | null>;
106
+ headObject(storageKey: string): Promise<PhisMediaObjectHead | null>;
107
+ deleteObject(storageKey: string): Promise<boolean>;
108
+ listPrefix(prefix: string): Promise<PhisMediaObjectHead[]>;
109
+ /** States how the Client is to deliver this body. */
110
+ createUploadPlan(input: PhiMediaUploadPlanInput): Promise<PhiMediaUploadPlan>;
111
+ /**
112
+ * Settles the staged object once the Client says it is done, and answers what actually landed.
113
+ *
114
+ * `null` means nothing is there, which is how Core tells an unfinished upload from a finished one.
115
+ * A Provider that assembles parts does it here; one that received the whole body simply looks.
116
+ */
117
+ completeUpload(input: PhiMediaUploadCompletionInput): Promise<PhisMediaObjectHead | null>;
118
+ /**
119
+ * States which browser origins may deliver a body straight to this Provider.
120
+ *
121
+ * A presigned plan is a cross-origin request, so without this the browser refuses to send it and no
122
+ * server ever learns why. The origins are Core's to know -- they are the Sites the Provider serves --
123
+ * and applying them is the Provider's, because only it knows how its storage expresses the rule.
124
+ *
125
+ * An empty list withdraws the permission rather than widening it. A Provider a browser never
126
+ * addresses directly, such as the Local one, implements this as a no-op; there is no capability flag
127
+ * to check, because a Provider that cannot say no to a body it never receives has nothing to say.
128
+ */
129
+ applyCorsPolicy(origins: readonly string[]): Promise<void>;
130
+ /**
131
+ * What makes two Storage Profiles the same physical storage.
132
+ *
133
+ * A CORS rule belongs to the storage, not to the Site, so two Sites sharing one bucket need one rule
134
+ * carrying both origins -- and applying each Site's rule in turn would leave whichever went last.
135
+ * Core cannot work this out: which configuration fields identify a storage and which merely divide
136
+ * it is the Provider's own business, and a prefix separates Sites inside one storage rather than
137
+ * making two.
138
+ *
139
+ * The value is opaque to Core and is only ever compared, so it must be stable across releases and
140
+ * must never contain a credential.
141
+ */
142
+ storageIdentity(): string;
143
+ }
144
+ /**
145
+ * What a Provider factory is handed besides its configuration.
146
+ *
147
+ * A Provider needs credentials and must not be able to reach anything else. The context therefore
148
+ * carries no database handle and no general secret accessor: `readSecret` resolves the one reference
149
+ * its own Storage Profile names, and there is no argument that could make it resolve another.
150
+ *
151
+ * It is asynchronous because a secret is fetched, and lazy because a Provider that never uploads
152
+ * anything should never need one. The Local Provider ignores the context entirely.
153
+ */
154
+ export type PhisMediaStorageServiceContext = {
155
+ providerId: PhisMediaStorageProviderId;
156
+ /** The Site whose profile this is, for diagnostics; not an authorization input. */
157
+ siteKey: string | null;
158
+ /** The profile's own secret, or null when the profile names none. */
159
+ readSecret: () => Promise<string | null>;
160
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The Media Storage service kind, as a Provider implements it.
3
+ *
4
+ * Core owns the upload lifecycle -- `init` and `finalize` stay Core routes for every Provider -- and a
5
+ * Provider states how a body is to be delivered and settles the staged object once it lands. Nothing
6
+ * here reaches a database, a request, or a secret store: the Provider gets configuration and a context,
7
+ * and answers about objects.
8
+ */
9
+ export const PHI_LOCAL_MEDIA_STORAGE_PROVIDER_ID = "@phis/server/storage-local";
@@ -0,0 +1,171 @@
1
+ /**
2
+ * What an Add-on says about the queries it runs.
3
+ *
4
+ * The same bargain as the schema descriptor, one step further: an Add-on ships no SQL for its tables
5
+ * and none for its statements either. It declares named queries, Core derives the statements once, and
6
+ * the Add-on invokes them by name with parameters. Nothing is assembled at request time, so `phis-cli`
7
+ * can show an operator every statement an installed Add-on is able to run -- which a query builder
8
+ * handed out at runtime could never do.
9
+ *
10
+ * The vocabulary is poorer than SQL on purpose, and poorer than the schema descriptor's in one further
11
+ * way: there are no joins. A join to a Core table would make the kernel's shape an Add-on's dependency,
12
+ * and a join between two Add-ons' tables is already forbidden. A query that needs one is either cut
13
+ * wrong or wants a Core service.
14
+ */
15
+ /** What a declared parameter holds. Narrower than the column vocabulary: these travel as values. */
16
+ export type PhiServerAddonQueryParameterType = "integer" | "bigInteger" | "text" | "boolean" | "instant" | "uuid";
17
+ export type PhiServerAddonQueryParameter = {
18
+ name: string;
19
+ type: PhiServerAddonQueryParameterType;
20
+ /** Whether the caller may pass null. Absent means it may not. */
21
+ nullable?: boolean;
22
+ };
23
+ /**
24
+ * Where a value in a query comes from.
25
+ *
26
+ * `actor` is the point of the list. "My rows" is the commonest filter there is, and letting an Add-on
27
+ * pass the acting user's id would mean trusting it to pass the right one. Core substitutes its own
28
+ * bound actor, so the query cannot be aimed at somebody else -- the same reason the capability closes
29
+ * over the Site instead of taking it as an argument.
30
+ */
31
+ export type PhiServerAddonQueryValue = {
32
+ kind: "parameter";
33
+ name: string;
34
+ } | {
35
+ kind: "literal";
36
+ value: string | number | boolean;
37
+ }
38
+ /** The acting user's id, substituted by Core. Null when nobody is acting, which matches nothing. */
39
+ | {
40
+ kind: "actor";
41
+ } | {
42
+ kind: "now";
43
+ };
44
+ export type PhiServerAddonQueryComparison = "=" | "<>" | "<" | "<=" | ">" | ">=";
45
+ /**
46
+ * A condition, as a tree of a closed set of shapes.
47
+ *
48
+ * No expressions, no functions, no pattern matching. `LIKE` is absent for the reason regular
49
+ * expressions are absent from the schema descriptor: its escaping and collation behaviour differ
50
+ * between database systems, and taking it would buy back the dependency this exists to remove. Text
51
+ * search is a Core service, not a query the Add-on writes.
52
+ */
53
+ export type PhiServerAddonQueryCondition = {
54
+ kind: "compare";
55
+ column: string;
56
+ operator: PhiServerAddonQueryComparison;
57
+ value: PhiServerAddonQueryValue;
58
+ } | {
59
+ kind: "isNull";
60
+ column: string;
61
+ negated?: boolean;
62
+ } | {
63
+ kind: "in";
64
+ column: string;
65
+ values: PhiServerAddonQueryValue[];
66
+ } | {
67
+ kind: "all";
68
+ conditions: PhiServerAddonQueryCondition[];
69
+ } | {
70
+ kind: "any";
71
+ conditions: PhiServerAddonQueryCondition[];
72
+ };
73
+ export type PhiServerAddonQueryOrdering = {
74
+ column: string;
75
+ direction: "asc" | "desc";
76
+ };
77
+ export type PhiServerAddonQueryAssignment = {
78
+ column: string;
79
+ value: PhiServerAddonQueryValue;
80
+ };
81
+ /**
82
+ * Reading rows of one of this Add-on's tables.
83
+ *
84
+ * `limit` is required and bounded. An unbounded read is how an Add-on takes an instance down with a
85
+ * table that grew, and "the caller will pass a sensible one" is not a bound.
86
+ */
87
+ export type PhiServerAddonSelectQueryDescriptor = {
88
+ kind: "select";
89
+ name: string;
90
+ table: string;
91
+ /** Absent means every declared column. Core's scope columns are never among them. */
92
+ columns?: string[];
93
+ where?: PhiServerAddonQueryCondition;
94
+ orderBy?: PhiServerAddonQueryOrdering[];
95
+ limit: number;
96
+ offsetParameter?: string;
97
+ };
98
+ /**
99
+ * Adding a row.
100
+ *
101
+ * The scope columns are absent by construction: `siteScoped`, `groupScoped` and `ownerScoped` mean Core
102
+ * writes them, so there is nowhere here to name one. Core fills the Site and the actor from the bound
103
+ * context; the group comes from a declared parameter because a row can belong to any group the actor is
104
+ * in, and Core refuses one they are not.
105
+ */
106
+ export type PhiServerAddonInsertQueryDescriptor = {
107
+ kind: "insert";
108
+ name: string;
109
+ table: string;
110
+ values: PhiServerAddonQueryAssignment[];
111
+ /** Names the parameter carrying the group id, and is required for a group-scoped table. */
112
+ groupParameter?: string;
113
+ };
114
+ /**
115
+ * Changing or removing exactly one row, addressed by its primary key.
116
+ *
117
+ * There is no `where` here and no bulk form. Core checks `mayActOnRow` against the row before it
118
+ * writes, and a check that has to be remembered is one that eventually is not -- so the shape makes it
119
+ * unskippable rather than the discipline. An Add-on that needs to rewrite many rows at once is doing
120
+ * something Core should be doing for it.
121
+ */
122
+ export type PhiServerAddonUpdateQueryDescriptor = {
123
+ kind: "update";
124
+ name: string;
125
+ table: string;
126
+ set: PhiServerAddonQueryAssignment[];
127
+ /** Names the parameter carrying the row's primary key value. */
128
+ idParameter: string;
129
+ };
130
+ export type PhiServerAddonDeleteQueryDescriptor = {
131
+ kind: "delete";
132
+ name: string;
133
+ table: string;
134
+ idParameter: string;
135
+ };
136
+ export type PhiServerAddonQueryDescriptor = PhiServerAddonSelectQueryDescriptor | PhiServerAddonInsertQueryDescriptor | PhiServerAddonUpdateQueryDescriptor | PhiServerAddonDeleteQueryDescriptor;
137
+ /** The most rows one declared `select` may return, whatever it asks for. */
138
+ export declare const PHI_SERVER_ADDON_QUERY_MAX_LIMIT: 500;
139
+ export type PhiServerAddonQueryCatalog = {
140
+ parameters: Readonly<Record<string, PhiServerAddonQueryParameter[]>>;
141
+ queries: PhiServerAddonQueryDescriptor[];
142
+ };
143
+ export type PhiServerAddonQueryArguments = Readonly<Record<string, string | number | boolean | null>>;
144
+ export type PhiServerAddonQueryRow = Readonly<Record<string, unknown>>;
145
+ /**
146
+ * Running the declared queries: `@phis/server/data:v1`.
147
+ *
148
+ * Every call names a query the manifest declared. There is no method that takes SQL, a table, or a
149
+ * condition, because an Add-on that could pass one of those at request time would be holding a
150
+ * database handle with extra steps.
151
+ *
152
+ * A mutation answers how many rows it touched. Zero from an `update` or `delete` means the row was not
153
+ * there or was not the caller's to change, and the two are deliberately the same answer: telling them
154
+ * apart would report the existence of a row the caller may not have.
155
+ */
156
+ export type PhiServerDataCapabilityV1 = {
157
+ select(name: string, args?: PhiServerAddonQueryArguments): Promise<PhiServerAddonQueryRow[]>;
158
+ /** Returns the inserted row, with the scope columns Core filled in. */
159
+ insert(name: string, args?: PhiServerAddonQueryArguments): Promise<PhiServerAddonQueryRow | null>;
160
+ update(name: string, args?: PhiServerAddonQueryArguments): Promise<number>;
161
+ delete(name: string, args?: PhiServerAddonQueryArguments): Promise<number>;
162
+ /**
163
+ * One unit of work. Everything inside commits together or not at all.
164
+ *
165
+ * The handle offers the same four operations and nothing else -- no commit, no rollback, no
166
+ * savepoint. Returning ends the transaction; throwing rolls it back. An Add-on cannot leave one
167
+ * open, because it never holds the thing that would stay open.
168
+ */
169
+ transaction<T>(body: (tx: PhiServerDataTransactionV1) => Promise<T>): Promise<T>;
170
+ };
171
+ export type PhiServerDataTransactionV1 = Omit<PhiServerDataCapabilityV1, "transaction">;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What an Add-on says about the queries it runs.
3
+ *
4
+ * The same bargain as the schema descriptor, one step further: an Add-on ships no SQL for its tables
5
+ * and none for its statements either. It declares named queries, Core derives the statements once, and
6
+ * the Add-on invokes them by name with parameters. Nothing is assembled at request time, so `phis-cli`
7
+ * can show an operator every statement an installed Add-on is able to run -- which a query builder
8
+ * handed out at runtime could never do.
9
+ *
10
+ * The vocabulary is poorer than SQL on purpose, and poorer than the schema descriptor's in one further
11
+ * way: there are no joins. A join to a Core table would make the kernel's shape an Add-on's dependency,
12
+ * and a join between two Add-ons' tables is already forbidden. A query that needs one is either cut
13
+ * wrong or wants a Core service.
14
+ */
15
+ /** The most rows one declared `select` may return, whatever it asks for. */
16
+ export const PHI_SERVER_ADDON_QUERY_MAX_LIMIT = 500;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * What an Add-on says about the tables it owns.
3
+ *
4
+ * An Add-on ships no SQL. It states the shape it wants and Core derives the statements, which is what
5
+ * lets an Add-on outlive a change of database system and what turns most of the rules in section 15 of
6
+ * the Add-on contract from something checked afterwards into something that cannot be expressed at all.
7
+ *
8
+ * The vocabulary is deliberately poorer than SQL. There are no triggers, functions, views, expression or
9
+ * partial indexes, and no free-form check, because each of those would be SQL again and would put back
10
+ * exactly what this removes. An Add-on that needs one of them is either cut wrong or needs a Core
11
+ * service.
12
+ */
13
+ export declare const PHI_SERVER_ADDON_SCHEMA_DESCRIPTOR_VERSION: 1;
14
+ /**
15
+ * What a column holds, said abstractly so Core can map it.
16
+ *
17
+ * `identity` is a generated key and may appear only in a primary key; the width is Core's business, not
18
+ * the Add-on's.
19
+ */
20
+ export type PhiServerAddonColumnType = "identity" | "smallInteger" | "integer" | "bigInteger" | "text" | "string" | "boolean" | "instant" | "json" | "uuid" | "binary";
21
+ /** The closed vocabulary a default may be drawn from. Anything wider would be an expression. */
22
+ export type PhiServerAddonColumnDefault = {
23
+ kind: "literal";
24
+ value: string | number | boolean;
25
+ } | {
26
+ kind: "now";
27
+ };
28
+ /**
29
+ * A Core table an Add-on may point at, named symbolically.
30
+ *
31
+ * An Add-on never writes `phis.sites(id)`. Core is then free to rename its own tables without breaking
32
+ * an installed Add-on, and which Core tables are referenceable becomes a decision instead of a
33
+ * consequence of what happens to be reachable.
34
+ *
35
+ * A group exists only inside a Site, so a table pointing at `core.group` must be Site-scoped and Core
36
+ * binds the pair rather than the id alone. Core never references a group any other way, and an Add-on
37
+ * that could would be able to name a group belonging to a different Site.
38
+ */
39
+ export declare const PHI_SERVER_ADDON_CORE_REFERENCES: {
40
+ readonly site: "core.site";
41
+ readonly user: "core.user";
42
+ readonly group: "core.group";
43
+ };
44
+ export type PhiServerAddonCoreReference = (typeof PHI_SERVER_ADDON_CORE_REFERENCES)[keyof typeof PHI_SERVER_ADDON_CORE_REFERENCES];
45
+ /**
46
+ * A delete rule, offered as two values and no third.
47
+ *
48
+ * An Add-on must not be able to refuse a Core deletion: an operator who cannot delete a user because an
49
+ * installed package forbids it is looking at a constraint error from code they may not know they have.
50
+ */
51
+ export type PhiServerAddonDeleteRule = "cascade" | "setNull";
52
+ export type PhiServerAddonValueColumnDescriptor = {
53
+ name: string;
54
+ type: PhiServerAddonColumnType;
55
+ /** Required for `string`, forbidden otherwise. */
56
+ maxLength?: number;
57
+ nullable?: boolean;
58
+ unique?: boolean;
59
+ default?: PhiServerAddonColumnDefault;
60
+ };
61
+ /**
62
+ * A column that points at something, and therefore has no type of its own.
63
+ *
64
+ * Its type is whatever it references, which is Core's to know: a Core key may be a different width than
65
+ * an Add-on would guess, and stating one here would be a guess that compiles. This is also the only
66
+ * shape a foreign key takes -- there is no separate list of them, and no composite key, because a
67
+ * reference that needs two columns is pointing at something an Add-on should not be reaching into.
68
+ */
69
+ export type PhiServerAddonReferenceColumnDescriptor = {
70
+ name: string;
71
+ /** A Core table under its symbolic name, or one of this Add-on's own tables. */
72
+ references: PhiServerAddonCoreReference | {
73
+ table: string;
74
+ };
75
+ nullable?: boolean;
76
+ unique?: boolean;
77
+ /** `setNull` requires a nullable column. */
78
+ onDelete: PhiServerAddonDeleteRule;
79
+ };
80
+ export type PhiServerAddonColumnDescriptor = PhiServerAddonValueColumnDescriptor | PhiServerAddonReferenceColumnDescriptor;
81
+ /**
82
+ * A constraint, in structured form.
83
+ *
84
+ * These belong in the first version of a descriptor and are the one part that does not wait: a column
85
+ * can be added later and an index can be added later, but a constraint added later meets the rows that
86
+ * are already there.
87
+ *
88
+ * Regular expressions are absent on purpose. Their syntax differs between database systems, and taking
89
+ * them would buy back the dependency this file exists to remove.
90
+ */
91
+ export type PhiServerAddonTableConstraintDescriptor = {
92
+ kind: "range";
93
+ name: string;
94
+ column: string;
95
+ min?: number;
96
+ max?: number;
97
+ exclusiveMin?: boolean;
98
+ exclusiveMax?: boolean;
99
+ } | {
100
+ kind: "enumeration";
101
+ name: string;
102
+ column: string;
103
+ values: string[];
104
+ } | {
105
+ kind: "notEmpty";
106
+ name: string;
107
+ column: string;
108
+ } | {
109
+ kind: "columnComparison";
110
+ name: string;
111
+ left: string;
112
+ operator: "<" | "<=" | ">" | ">=";
113
+ right: string;
114
+ }
115
+ /** For the polymorphic row: exactly `nonNullCount` of `columns` is set. */
116
+ | {
117
+ kind: "nullCardinality";
118
+ name: string;
119
+ columns: string[];
120
+ nonNullCount: number;
121
+ };
122
+ export type PhiServerAddonIndexDescriptor = {
123
+ name: string;
124
+ columns: string[];
125
+ unique?: boolean;
126
+ };
127
+ export type PhiServerAddonTableDescriptor = {
128
+ name: string;
129
+ /**
130
+ * Core adds the Site column, its foreign key, and its cascade, and puts the Site filter into every
131
+ * query that reads this table. The Add-on neither writes the column nor is able to forget it.
132
+ *
133
+ * This is more than a reference column: it is the boundary a query is compiled against. A table that
134
+ * merely wants to mention a Site uses a reference column instead.
135
+ */
136
+ siteScoped?: boolean;
137
+ /**
138
+ * Core adds the owner column and its foreign key, and answers whether a row belongs to the acting
139
+ * user. The Add-on neither writes the column nor decides what "own" means, which is the point: the
140
+ * rule that a person may edit and delete what they created is one rule, not one per package.
141
+ *
142
+ * The column is nullable and a deleted user leaves it null rather than taking the rows along. A row
143
+ * without an owner therefore belongs to nobody, and no membership level reaches it through ownership.
144
+ *
145
+ * This is authority, not history. A table that only wants to record who created a row uses an
146
+ * ordinary `core.user` reference column, which grants nothing.
147
+ */
148
+ ownerScoped?: boolean;
149
+ /**
150
+ * Core adds the group column and binds it to the Site's group, which makes the group membership
151
+ * levels applicable to this table. Requires `siteScoped`, because a group has no meaning without one.
152
+ *
153
+ * As with the Site, this is more than a reference column: it says the rows are group property and are
154
+ * read through a membership level. A table that merely wants to mention a group uses a `core.group`
155
+ * reference column instead.
156
+ */
157
+ groupScoped?: boolean;
158
+ columns: PhiServerAddonColumnDescriptor[];
159
+ primaryKey: string[];
160
+ indexes?: PhiServerAddonIndexDescriptor[];
161
+ constraints?: PhiServerAddonTableConstraintDescriptor[];
162
+ };
163
+ /**
164
+ * The shape an Add-on's schema should have -- not the steps to reach it.
165
+ *
166
+ * `version` is the schema version the manifest reports, and Core compares this description against the
167
+ * one it recorded to decide what to run.
168
+ */
169
+ export type PhiServerAddonSchemaDescriptor = {
170
+ descriptorVersion: typeof PHI_SERVER_ADDON_SCHEMA_DESCRIPTOR_VERSION;
171
+ version: number;
172
+ tables: PhiServerAddonTableDescriptor[];
173
+ };
package/dist/schema.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * What an Add-on says about the tables it owns.
3
+ *
4
+ * An Add-on ships no SQL. It states the shape it wants and Core derives the statements, which is what
5
+ * lets an Add-on outlive a change of database system and what turns most of the rules in section 15 of
6
+ * the Add-on contract from something checked afterwards into something that cannot be expressed at all.
7
+ *
8
+ * The vocabulary is deliberately poorer than SQL. There are no triggers, functions, views, expression or
9
+ * partial indexes, and no free-form check, because each of those would be SQL again and would put back
10
+ * exactly what this removes. An Add-on that needs one of them is either cut wrong or needs a Core
11
+ * service.
12
+ */
13
+ export const PHI_SERVER_ADDON_SCHEMA_DESCRIPTOR_VERSION = 1;
14
+ /**
15
+ * A Core table an Add-on may point at, named symbolically.
16
+ *
17
+ * An Add-on never writes `phis.sites(id)`. Core is then free to rename its own tables without breaking
18
+ * an installed Add-on, and which Core tables are referenceable becomes a decision instead of a
19
+ * consequence of what happens to be reachable.
20
+ *
21
+ * A group exists only inside a Site, so a table pointing at `core.group` must be Site-scoped and Core
22
+ * binds the pair rather than the id alone. Core never references a group any other way, and an Add-on
23
+ * that could would be able to name a group belonging to a different Site.
24
+ */
25
+ export const PHI_SERVER_ADDON_CORE_REFERENCES = {
26
+ site: "core.site",
27
+ user: "core.user",
28
+ group: "core.group",
29
+ };