@impetik/xeer-mcp 0.2.5 → 0.2.7
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/README.md +4 -1
- package/dist/dev-session.d.ts +1 -1
- package/dist/network-policy.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +4 -4
- package/dist/test-run.d.ts +1 -1
- package/dist/xeer-cli.d.ts +1 -1
- package/package.json +8 -5
- package/vendor/spec/actions.d.ts +1250 -0
- package/vendor/spec/actions.js +805 -0
- package/vendor/spec/admin-sql.d.ts +59 -0
- package/vendor/spec/admin-sql.js +147 -0
- package/vendor/spec/admin.d.ts +110 -0
- package/vendor/spec/admin.js +58 -0
- package/vendor/spec/canonical.d.ts +3 -0
- package/vendor/spec/canonical.js +36 -0
- package/vendor/spec/diagnostics.d.ts +49 -0
- package/vendor/spec/diagnostics.js +500 -0
- package/vendor/spec/docs.d.ts +21 -0
- package/vendor/spec/docs.js +57 -0
- package/vendor/spec/events.d.ts +8 -0
- package/vendor/spec/events.js +21 -0
- package/vendor/spec/identity-keys.d.ts +36 -0
- package/vendor/spec/identity-keys.js +72 -0
- package/vendor/spec/index.d.ts +20 -0
- package/vendor/spec/index.js +20 -0
- package/vendor/spec/local-identity.d.ts +69 -0
- package/vendor/spec/local-identity.js +132 -0
- package/vendor/spec/network-policy.d.ts +16 -0
- package/vendor/spec/network-policy.js +50 -0
- package/vendor/spec/public-assets.d.ts +153 -0
- package/vendor/spec/public-assets.js +166 -0
- package/vendor/spec/review.d.ts +120 -0
- package/vendor/spec/review.js +226 -0
- package/vendor/spec/route.d.ts +43 -0
- package/vendor/spec/route.js +87 -0
- package/vendor/spec/schema-lifecycle.d.ts +27 -0
- package/vendor/spec/schema-lifecycle.js +146 -0
- package/vendor/spec/schema-plan.d.ts +98 -0
- package/vendor/spec/schema-plan.js +194 -0
- package/vendor/spec/schema.d.ts +166 -0
- package/vendor/spec/schema.js +409 -0
- package/vendor/spec/sql-expression.d.ts +91 -0
- package/vendor/spec/sql-expression.js +650 -0
- package/vendor/spec/state-export.d.ts +143 -0
- package/vendor/spec/state-export.js +341 -0
- package/vendor/spec/storage.d.ts +61 -0
- package/vendor/spec/storage.js +120 -0
- package/vendor/spec/table-ddl.d.ts +162 -0
- package/vendor/spec/table-ddl.js +508 -0
- package/vendor/spec/types.d.ts +275 -0
- package/vendor/spec/types.js +11 -0
- package/vendor/spec/value.d.ts +22 -0
- package/vendor/spec/value.js +72 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApplicationSchemaIdentityV0 } from './types.js';
|
|
2
|
+
import { type NormalizedDatabaseSchemaV0, type SchemaPlanV0 } from './schema-plan.js';
|
|
3
|
+
export * from './schema-plan.js';
|
|
4
|
+
export declare function applicationSchemaHash(schema: NormalizedDatabaseSchemaV0): `sha256:${string}`;
|
|
5
|
+
export declare function applicationSchemaIdentity(schema: NormalizedDatabaseSchemaV0): ApplicationSchemaIdentityV0;
|
|
6
|
+
/** A fresh copy of the reference schema, so nothing that reads it can move the revision for everyone. */
|
|
7
|
+
export declare function schemaIdentityReference(): NormalizedDatabaseSchemaV0;
|
|
8
|
+
/**
|
|
9
|
+
* Which schema-identity projection *this build* computes, as the hash it gives the reference schema
|
|
10
|
+
* above.
|
|
11
|
+
*
|
|
12
|
+
* A schema hash is only an identity because two builds agree on it, and when they stop agreeing every
|
|
13
|
+
* symptom is somewhere else: the control plane refuses a `schemaIdentity` the CLI computed correctly
|
|
14
|
+
* for its own build, and the deployment reads as a malformed bundle (#205). Nothing about a version
|
|
15
|
+
* number settles it either way — a release can move the projection or leave it untouched — so the
|
|
16
|
+
* answer has to be a value derived from the projection itself. Two builds reporting the same revision
|
|
17
|
+
* hash the same schema to the same identity; two reporting different ones do not, and the older is the
|
|
18
|
+
* one to upgrade.
|
|
19
|
+
*
|
|
20
|
+
* Derived rather than declared, so it cannot be forgotten: adding, removing or renaming a key in
|
|
21
|
+
* `normalizedSchemaPayload` moves this hash in the same commit that moves it, because every key is
|
|
22
|
+
* emitted unconditionally and the reference declares one of each. What it cannot see is a change to how
|
|
23
|
+
* a value is *derived* for a shape the reference does not declare — index normalization has its own
|
|
24
|
+
* spellings, and only the ones written here are covered.
|
|
25
|
+
*/
|
|
26
|
+
export declare function schemaIdentityRevision(): `sha256:${string}`;
|
|
27
|
+
export declare function planSchemaChange(application: string, fromSchema: NormalizedDatabaseSchemaV0, toSchema: NormalizedDatabaseSchemaV0): SchemaPlanV0;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { canonicalHash } from './canonical.js';
|
|
2
|
+
import { normalizedIndex } from './table-ddl.js';
|
|
3
|
+
import { planSchemaChangeWithIdentities, } from './schema-plan.js';
|
|
4
|
+
export * from './schema-plan.js';
|
|
5
|
+
/**
|
|
6
|
+
* The projection of a schema that its hash is taken over.
|
|
7
|
+
*
|
|
8
|
+
* Every key here is one that changes what the database will accept, store, or return: the hash is
|
|
9
|
+
* the identity of the *contract*, and two schemas that hold data to different rules must not share
|
|
10
|
+
* one. That is why nothing is omitted as a detail — a collation decides which values collide under
|
|
11
|
+
* a UNIQUE, a default decides what an omitted column receives, an index's direction and predicate
|
|
12
|
+
* decide which physical index has to exist. A key that only changed how a schema *reads* — a label,
|
|
13
|
+
* a description — would belong outside this projection, and there is no such key today.
|
|
14
|
+
*
|
|
15
|
+
* The projection is explicit rather than a spread of the definition so that adding a key to the
|
|
16
|
+
* manifest is a decision made here, in one place, instead of a hash that quietly moves.
|
|
17
|
+
*/
|
|
18
|
+
function normalizedSchemaPayload(schema) {
|
|
19
|
+
return {
|
|
20
|
+
tables: Object.fromEntries(Object.keys(schema.tables).sort().map((tableName) => {
|
|
21
|
+
const table = schema.tables[tableName];
|
|
22
|
+
return [tableName, {
|
|
23
|
+
fields: Object.fromEntries(Object.keys(table.fields).sort().map((fieldName) => {
|
|
24
|
+
const field = table.fields[fieldName];
|
|
25
|
+
return [fieldName, {
|
|
26
|
+
type: field.type,
|
|
27
|
+
optional: field.optional ?? false,
|
|
28
|
+
maxLength: field.maxLength ?? null,
|
|
29
|
+
unique: field.unique ?? false,
|
|
30
|
+
enum: field.enum === undefined ? null : [...field.enum],
|
|
31
|
+
default: field.default ?? null,
|
|
32
|
+
collate: field.collate ?? null,
|
|
33
|
+
table: field.table ?? null,
|
|
34
|
+
onDelete: field.onDelete ?? null,
|
|
35
|
+
onUpdate: field.onUpdate ?? null,
|
|
36
|
+
generated: field.generated === undefined
|
|
37
|
+
? null
|
|
38
|
+
: { expression: field.generated.expression, stored: field.generated.stored ?? false },
|
|
39
|
+
}];
|
|
40
|
+
})),
|
|
41
|
+
// Normalized, so the shorthand and the full form of one index hash the same.
|
|
42
|
+
indexes: Object.fromEntries(Object.keys(table.indexes ?? {}).sort()
|
|
43
|
+
.map((indexName) => [indexName, normalizedIndex(table.indexes[indexName])])),
|
|
44
|
+
unique: (table.unique ?? []).map((tuple) => [...tuple]),
|
|
45
|
+
checks: Object.fromEntries(Object.keys(table.checks ?? {}).sort()
|
|
46
|
+
.map((checkName) => [checkName, table.checks[checkName]])),
|
|
47
|
+
}];
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function applicationSchemaHash(schema) {
|
|
52
|
+
return canonicalHash(normalizedSchemaPayload(schema));
|
|
53
|
+
}
|
|
54
|
+
export function applicationSchemaIdentity(schema) {
|
|
55
|
+
return { applicationSchemaVersion: schema.version, schemaHash: applicationSchemaHash(schema) };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A fixed schema declaring one of everything the projection above reads, so that hashing it says which
|
|
59
|
+
* projection a build carries.
|
|
60
|
+
*
|
|
61
|
+
* Every attribute sits on a field named after it, which is what lets the test delete them one at a time
|
|
62
|
+
* and prove the revision notices. The declaration is a hash probe and not a manifest: `onDelete` and
|
|
63
|
+
* `onUpdate` need a `ref` field to sit on, so there are three of those, and nothing here is validated
|
|
64
|
+
* against the manifest rules because nothing here is ever deployed.
|
|
65
|
+
*
|
|
66
|
+
* Two properties of the *shape* are load-bearing, and both exist to close a way the revision could
|
|
67
|
+
* otherwise agree while the builds do not.
|
|
68
|
+
*
|
|
69
|
+
* **Two of everything the projection can iterate.** One table, one check, one index, one unique tuple
|
|
70
|
+
* would each let a projection that read only the first of them hash this schema exactly as a correct
|
|
71
|
+
* one does.
|
|
72
|
+
*
|
|
73
|
+
* **Every order-preserving array declared in an order sorting would change.** Object keys are sorted by
|
|
74
|
+
* canonical JSON and cannot carry order, but four arrays can and all four are semantic: an enum's
|
|
75
|
+
* values, the outer and inner arrays of a composite UNIQUE, and an index's columns — `UNIQUE (a, b)`
|
|
76
|
+
* and `UNIQUE (b, a)` build different indexes. A reference declaring a single unique tuple, or a tuple
|
|
77
|
+
* that happened to be in sorted order, would survive a future commit that started sorting them
|
|
78
|
+
* unchanged, while the hash of every real schema with two moved. Two builds would then compute
|
|
79
|
+
* different schema identities and report the *same* revision, and a `schema_identity_mismatch` would
|
|
80
|
+
* quote it as proof that they agree.
|
|
81
|
+
*/
|
|
82
|
+
const SCHEMA_IDENTITY_REFERENCE = {
|
|
83
|
+
version: 1,
|
|
84
|
+
tables: {
|
|
85
|
+
reference: {
|
|
86
|
+
fields: {
|
|
87
|
+
bytes: { type: 'bytes' },
|
|
88
|
+
collate: { type: 'string', collate: 'nocase' },
|
|
89
|
+
datetime: { type: 'datetime' },
|
|
90
|
+
default: { type: 'string', default: 'a' },
|
|
91
|
+
enum: { type: 'string', enum: ['b', 'a'] },
|
|
92
|
+
generated: { type: 'string', generated: { expression: 'lower(plain)', stored: true } },
|
|
93
|
+
json: { type: 'json' },
|
|
94
|
+
maxLength: { type: 'string', maxLength: 64 },
|
|
95
|
+
number: { type: 'number' },
|
|
96
|
+
onDelete: { type: 'ref', table: 'reference', onDelete: 'cascade' },
|
|
97
|
+
onUpdate: { type: 'ref', table: 'reference', onUpdate: 'restrict' },
|
|
98
|
+
optional: { type: 'string', optional: true },
|
|
99
|
+
plain: { type: 'string' },
|
|
100
|
+
table: { type: 'ref', table: 'reference' },
|
|
101
|
+
unique: { type: 'string', unique: true },
|
|
102
|
+
},
|
|
103
|
+
indexes: {
|
|
104
|
+
by_plain: ['plain'],
|
|
105
|
+
by_pair: ['plain', 'number'],
|
|
106
|
+
by_unique: { columns: [{ column: 'unique', desc: true }], unique: true, where: 'plain IS NOT NULL' },
|
|
107
|
+
},
|
|
108
|
+
unique: [['plain', 'number'], ['optional', 'maxLength']],
|
|
109
|
+
checks: { positive: 'number > 0', bounded: 'maxLength IS NOT NULL' },
|
|
110
|
+
},
|
|
111
|
+
beta: {
|
|
112
|
+
fields: { second: { type: 'string' }, first: { type: 'number' } },
|
|
113
|
+
indexes: { by_second: ['second'], by_first: ['first'] },
|
|
114
|
+
unique: [['second', 'first']],
|
|
115
|
+
checks: { nonzero: 'first <> 0', named: 'second <> \'\'' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
/** A fresh copy of the reference schema, so nothing that reads it can move the revision for everyone. */
|
|
120
|
+
export function schemaIdentityReference() {
|
|
121
|
+
return structuredClone(SCHEMA_IDENTITY_REFERENCE);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Which schema-identity projection *this build* computes, as the hash it gives the reference schema
|
|
125
|
+
* above.
|
|
126
|
+
*
|
|
127
|
+
* A schema hash is only an identity because two builds agree on it, and when they stop agreeing every
|
|
128
|
+
* symptom is somewhere else: the control plane refuses a `schemaIdentity` the CLI computed correctly
|
|
129
|
+
* for its own build, and the deployment reads as a malformed bundle (#205). Nothing about a version
|
|
130
|
+
* number settles it either way — a release can move the projection or leave it untouched — so the
|
|
131
|
+
* answer has to be a value derived from the projection itself. Two builds reporting the same revision
|
|
132
|
+
* hash the same schema to the same identity; two reporting different ones do not, and the older is the
|
|
133
|
+
* one to upgrade.
|
|
134
|
+
*
|
|
135
|
+
* Derived rather than declared, so it cannot be forgotten: adding, removing or renaming a key in
|
|
136
|
+
* `normalizedSchemaPayload` moves this hash in the same commit that moves it, because every key is
|
|
137
|
+
* emitted unconditionally and the reference declares one of each. What it cannot see is a change to how
|
|
138
|
+
* a value is *derived* for a shape the reference does not declare — index normalization has its own
|
|
139
|
+
* spellings, and only the ones written here are covered.
|
|
140
|
+
*/
|
|
141
|
+
export function schemaIdentityRevision() {
|
|
142
|
+
return applicationSchemaHash(SCHEMA_IDENTITY_REFERENCE);
|
|
143
|
+
}
|
|
144
|
+
export function planSchemaChange(application, fromSchema, toSchema) {
|
|
145
|
+
return planSchemaChangeWithIdentities(application, fromSchema, applicationSchemaIdentity(fromSchema), toSchema, applicationSchemaIdentity(toSchema));
|
|
146
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ApplicationSchemaIdentityV0, NormalizedApplicationManifestV0, ScalarType } from './types.js';
|
|
2
|
+
export declare const SCHEMA_PROTOCOL: "xeer.schema.v0";
|
|
3
|
+
export type NormalizedDatabaseSchemaV0 = NormalizedApplicationManifestV0['database'];
|
|
4
|
+
/**
|
|
5
|
+
* Field keys with no dedicated step. Each one changes what the column's DDL says, so a change to any
|
|
6
|
+
* of them has to reach the plan — otherwise a schema whose hash moved would produce no steps and be
|
|
7
|
+
* classified as an unchanged schema that somehow needs a version bump.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FIELD_ATTRIBUTES: readonly ["collate", "default", "enum", "generated", "onDelete", "onUpdate", "table", "unique"];
|
|
10
|
+
export type FieldAttribute = (typeof FIELD_ATTRIBUTES)[number];
|
|
11
|
+
export type SchemaPlanStepV0 = {
|
|
12
|
+
kind: 'table.add';
|
|
13
|
+
table: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'table.remove';
|
|
16
|
+
table: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'table.constraint';
|
|
19
|
+
table: string;
|
|
20
|
+
constraint: 'unique' | 'checks';
|
|
21
|
+
from: unknown;
|
|
22
|
+
to: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'field.add';
|
|
25
|
+
table: string;
|
|
26
|
+
field: string;
|
|
27
|
+
optional: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'field.remove';
|
|
30
|
+
table: string;
|
|
31
|
+
field: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'field.type';
|
|
34
|
+
table: string;
|
|
35
|
+
field: string;
|
|
36
|
+
from: ScalarType;
|
|
37
|
+
to: ScalarType;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'field.optional';
|
|
40
|
+
table: string;
|
|
41
|
+
field: string;
|
|
42
|
+
from: boolean;
|
|
43
|
+
to: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'field.maxLength';
|
|
46
|
+
table: string;
|
|
47
|
+
field: string;
|
|
48
|
+
from: number | null;
|
|
49
|
+
to: number | null;
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'field.attribute';
|
|
52
|
+
table: string;
|
|
53
|
+
field: string;
|
|
54
|
+
attribute: FieldAttribute;
|
|
55
|
+
from: unknown;
|
|
56
|
+
to: unknown;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'index.add';
|
|
59
|
+
table: string;
|
|
60
|
+
index: string;
|
|
61
|
+
fields: string[];
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'index.remove';
|
|
64
|
+
table: string;
|
|
65
|
+
index: string;
|
|
66
|
+
fields: string[];
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'index.change';
|
|
69
|
+
table: string;
|
|
70
|
+
index: string;
|
|
71
|
+
from: string[];
|
|
72
|
+
to: string[];
|
|
73
|
+
} | {
|
|
74
|
+
kind: 'index.attribute';
|
|
75
|
+
table: string;
|
|
76
|
+
index: string;
|
|
77
|
+
attribute: 'unique' | 'where';
|
|
78
|
+
from: unknown;
|
|
79
|
+
to: unknown;
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'unknown.change';
|
|
82
|
+
path: string;
|
|
83
|
+
};
|
|
84
|
+
export interface SchemaPlanV0 {
|
|
85
|
+
protocol: typeof SCHEMA_PROTOCOL;
|
|
86
|
+
application: string;
|
|
87
|
+
from: ApplicationSchemaIdentityV0;
|
|
88
|
+
to: ApplicationSchemaIdentityV0;
|
|
89
|
+
classification: 'unchanged' | 'compatible' | 'incompatible';
|
|
90
|
+
steps: SchemaPlanStepV0[];
|
|
91
|
+
requiresReset: boolean;
|
|
92
|
+
}
|
|
93
|
+
export type SchemaPlanningErrorCode = 'schema_version_regressed' | 'schema_version_unchanged' | 'schema_version_skipped';
|
|
94
|
+
export declare class SchemaPlanningError extends Error {
|
|
95
|
+
readonly code: SchemaPlanningErrorCode;
|
|
96
|
+
constructor(code: SchemaPlanningErrorCode, message: string);
|
|
97
|
+
}
|
|
98
|
+
export declare function planSchemaChangeWithIdentities(application: string, fromSchema: NormalizedDatabaseSchemaV0, from: ApplicationSchemaIdentityV0, toSchema: NormalizedDatabaseSchemaV0, to: ApplicationSchemaIdentityV0): SchemaPlanV0;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { indexTermLabel, normalizedIndex } from './table-ddl.js';
|
|
2
|
+
export const SCHEMA_PROTOCOL = 'xeer.schema.v0';
|
|
3
|
+
/**
|
|
4
|
+
* Field keys with no dedicated step. Each one changes what the column's DDL says, so a change to any
|
|
5
|
+
* of them has to reach the plan — otherwise a schema whose hash moved would produce no steps and be
|
|
6
|
+
* classified as an unchanged schema that somehow needs a version bump.
|
|
7
|
+
*/
|
|
8
|
+
export const FIELD_ATTRIBUTES = Object.freeze([
|
|
9
|
+
'collate', 'default', 'enum', 'generated', 'onDelete', 'onUpdate', 'table', 'unique',
|
|
10
|
+
]);
|
|
11
|
+
export class SchemaPlanningError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'SchemaPlanningError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function sameArray(left, right) {
|
|
20
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
21
|
+
}
|
|
22
|
+
function maxLengthStep(table, field, from, to) {
|
|
23
|
+
if (from === to)
|
|
24
|
+
return null;
|
|
25
|
+
return { kind: 'field.maxLength', table, field, from: from ?? null, to: to ?? null };
|
|
26
|
+
}
|
|
27
|
+
function tableSteps(tableName, from, to) {
|
|
28
|
+
const steps = [];
|
|
29
|
+
const fromFields = new Set(Object.keys(from.fields));
|
|
30
|
+
const toFields = new Set(Object.keys(to.fields));
|
|
31
|
+
for (const field of [...fromFields].filter((name) => !toFields.has(name)).sort()) {
|
|
32
|
+
steps.push({ kind: 'field.remove', table: tableName, field });
|
|
33
|
+
}
|
|
34
|
+
for (const field of [...toFields].filter((name) => !fromFields.has(name)).sort()) {
|
|
35
|
+
steps.push({ kind: 'field.add', table: tableName, field, optional: to.fields[field].optional ?? false });
|
|
36
|
+
}
|
|
37
|
+
for (const field of [...fromFields].filter((name) => toFields.has(name)).sort()) {
|
|
38
|
+
const before = from.fields[field];
|
|
39
|
+
const after = to.fields[field];
|
|
40
|
+
if (before.type !== after.type) {
|
|
41
|
+
steps.push({ kind: 'field.type', table: tableName, field, from: before.type, to: after.type });
|
|
42
|
+
}
|
|
43
|
+
const beforeOptional = before.optional ?? false;
|
|
44
|
+
const afterOptional = after.optional ?? false;
|
|
45
|
+
if (beforeOptional !== afterOptional) {
|
|
46
|
+
steps.push({ kind: 'field.optional', table: tableName, field, from: beforeOptional, to: afterOptional });
|
|
47
|
+
}
|
|
48
|
+
const length = maxLengthStep(tableName, field, before.maxLength, after.maxLength);
|
|
49
|
+
if (length)
|
|
50
|
+
steps.push(length);
|
|
51
|
+
for (const attribute of FIELD_ATTRIBUTES) {
|
|
52
|
+
if (sameUnknownValue(before[attribute], after[attribute]))
|
|
53
|
+
continue;
|
|
54
|
+
steps.push({ kind: 'field.attribute', table: tableName, field, attribute,
|
|
55
|
+
from: before[attribute] ?? null, to: after[attribute] ?? null });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const constraint of ['unique', 'checks']) {
|
|
59
|
+
if (sameUnknownValue(from[constraint], to[constraint]))
|
|
60
|
+
continue;
|
|
61
|
+
steps.push({ kind: 'table.constraint', table: tableName, constraint,
|
|
62
|
+
from: from[constraint] ?? null, to: to[constraint] ?? null });
|
|
63
|
+
}
|
|
64
|
+
// Indexes are compared through their normalized form, so `["slug"]` and `{ columns: ["slug"] }`
|
|
65
|
+
// are one index written two ways rather than a change from one to the other.
|
|
66
|
+
const terms = (index) => normalizedIndex(index).columns.map(indexTermLabel);
|
|
67
|
+
const fromIndexes = from.indexes ?? {};
|
|
68
|
+
const toIndexes = to.indexes ?? {};
|
|
69
|
+
const fromNames = new Set(Object.keys(fromIndexes));
|
|
70
|
+
const toNames = new Set(Object.keys(toIndexes));
|
|
71
|
+
for (const index of [...fromNames].filter((name) => !toNames.has(name)).sort()) {
|
|
72
|
+
steps.push({ kind: 'index.remove', table: tableName, index, fields: terms(fromIndexes[index]) });
|
|
73
|
+
}
|
|
74
|
+
for (const index of [...toNames].filter((name) => !fromNames.has(name)).sort()) {
|
|
75
|
+
steps.push({ kind: 'index.add', table: tableName, index, fields: terms(toIndexes[index]) });
|
|
76
|
+
}
|
|
77
|
+
for (const index of [...fromNames].filter((name) => toNames.has(name)).sort()) {
|
|
78
|
+
const before = normalizedIndex(fromIndexes[index]);
|
|
79
|
+
const after = normalizedIndex(toIndexes[index]);
|
|
80
|
+
if (!sameArray(terms(fromIndexes[index]), terms(toIndexes[index]))) {
|
|
81
|
+
steps.push({ kind: 'index.change', table: tableName, index,
|
|
82
|
+
from: terms(fromIndexes[index]), to: terms(toIndexes[index]) });
|
|
83
|
+
}
|
|
84
|
+
for (const attribute of ['unique', 'where']) {
|
|
85
|
+
if (before[attribute] === after[attribute])
|
|
86
|
+
continue;
|
|
87
|
+
steps.push({ kind: 'index.attribute', table: tableName, index, attribute,
|
|
88
|
+
from: before[attribute], to: after[attribute] });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return steps;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether a step leaves the rows already stored valid. Nothing in the extended vocabulary does,
|
|
95
|
+
* with one exception: a DEFAULT decides what a later insert that omits the column receives and says
|
|
96
|
+
* nothing about the rows already there, so changing one cannot invalidate them.
|
|
97
|
+
*/
|
|
98
|
+
function isCompatible(step) {
|
|
99
|
+
if (step.kind === 'table.add' || step.kind === 'index.add')
|
|
100
|
+
return true;
|
|
101
|
+
if (step.kind === 'field.add')
|
|
102
|
+
return step.optional;
|
|
103
|
+
if (step.kind === 'field.optional')
|
|
104
|
+
return step.from === false && step.to === true;
|
|
105
|
+
if (step.kind === 'field.maxLength') {
|
|
106
|
+
return step.from !== null && (step.to === null || step.to > step.from);
|
|
107
|
+
}
|
|
108
|
+
if (step.kind === 'field.attribute')
|
|
109
|
+
return step.attribute === 'default';
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function sameUnknownValue(left, right) {
|
|
113
|
+
if (Object.is(left, right))
|
|
114
|
+
return true;
|
|
115
|
+
if (typeof left !== typeof right || left === null || right === null)
|
|
116
|
+
return false;
|
|
117
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
118
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length
|
|
119
|
+
&& left.every((value, index) => sameUnknownValue(value, right[index]));
|
|
120
|
+
}
|
|
121
|
+
if (typeof left !== 'object')
|
|
122
|
+
return false;
|
|
123
|
+
const before = left;
|
|
124
|
+
const after = right;
|
|
125
|
+
const beforeKeys = Object.keys(before).sort();
|
|
126
|
+
const afterKeys = Object.keys(after).sort();
|
|
127
|
+
return sameArray(beforeKeys, afterKeys)
|
|
128
|
+
&& beforeKeys.every((key) => sameUnknownValue(before[key], after[key]));
|
|
129
|
+
}
|
|
130
|
+
function unknownKeySteps(path, from, to, allowed) {
|
|
131
|
+
const before = typeof from === 'object' && from !== null && !Array.isArray(from)
|
|
132
|
+
? from : {};
|
|
133
|
+
const after = typeof to === 'object' && to !== null && !Array.isArray(to)
|
|
134
|
+
? to : {};
|
|
135
|
+
const known = new Set(allowed);
|
|
136
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
137
|
+
.filter((key) => !known.has(key))
|
|
138
|
+
.sort()
|
|
139
|
+
.filter((key) => {
|
|
140
|
+
const beforeHas = Object.hasOwn(before, key);
|
|
141
|
+
const afterHas = Object.hasOwn(after, key);
|
|
142
|
+
return beforeHas !== afterHas || !sameUnknownValue(before[key], after[key]);
|
|
143
|
+
})
|
|
144
|
+
.map((key) => ({ kind: 'unknown.change', path: `${path}.${key}` }));
|
|
145
|
+
}
|
|
146
|
+
function unknownSchemaSteps(fromSchema, toSchema) {
|
|
147
|
+
const steps = unknownKeySteps('database', fromSchema, toSchema, ['version', 'tables']);
|
|
148
|
+
const tableNames = [...new Set([...Object.keys(fromSchema.tables), ...Object.keys(toSchema.tables)])].sort();
|
|
149
|
+
for (const tableName of tableNames) {
|
|
150
|
+
const beforeTable = fromSchema.tables[tableName];
|
|
151
|
+
const afterTable = toSchema.tables[tableName];
|
|
152
|
+
steps.push(...unknownKeySteps(`database.tables.${tableName}`, beforeTable, afterTable, ['fields', 'indexes', 'unique', 'checks']));
|
|
153
|
+
const beforeFields = beforeTable?.fields ?? {};
|
|
154
|
+
const afterFields = afterTable?.fields ?? {};
|
|
155
|
+
const fieldNames = [...new Set([...Object.keys(beforeFields), ...Object.keys(afterFields)])].sort();
|
|
156
|
+
for (const fieldName of fieldNames) {
|
|
157
|
+
steps.push(...unknownKeySteps(`database.tables.${tableName}.fields.${fieldName}`, beforeFields[fieldName], afterFields[fieldName], ['type', 'optional', 'maxLength', ...FIELD_ATTRIBUTES]));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return steps;
|
|
161
|
+
}
|
|
162
|
+
export function planSchemaChangeWithIdentities(application, fromSchema, from, toSchema, to) {
|
|
163
|
+
const unknownSteps = unknownSchemaSteps(fromSchema, toSchema);
|
|
164
|
+
if (to.applicationSchemaVersion < from.applicationSchemaVersion) {
|
|
165
|
+
throw new SchemaPlanningError('schema_version_regressed', `Application schema version cannot regress from ${from.applicationSchemaVersion} to ${to.applicationSchemaVersion}.`);
|
|
166
|
+
}
|
|
167
|
+
if (to.applicationSchemaVersion === from.applicationSchemaVersion
|
|
168
|
+
&& (to.schemaHash !== from.schemaHash || unknownSteps.length > 0)) {
|
|
169
|
+
throw new SchemaPlanningError('schema_version_unchanged', `Application schema changed without increasing database.version from ${from.applicationSchemaVersion}.`);
|
|
170
|
+
}
|
|
171
|
+
if (to.applicationSchemaVersion > from.applicationSchemaVersion + 1) {
|
|
172
|
+
throw new SchemaPlanningError('schema_version_skipped', `Application schema version must advance one step from ${from.applicationSchemaVersion}; received ${to.applicationSchemaVersion}.`);
|
|
173
|
+
}
|
|
174
|
+
if (to.schemaHash === from.schemaHash && unknownSteps.length === 0
|
|
175
|
+
&& to.applicationSchemaVersion === from.applicationSchemaVersion) {
|
|
176
|
+
return { protocol: SCHEMA_PROTOCOL, application, from, to,
|
|
177
|
+
classification: 'unchanged', steps: [], requiresReset: false };
|
|
178
|
+
}
|
|
179
|
+
const steps = [...unknownSteps];
|
|
180
|
+
const fromTables = new Set(Object.keys(fromSchema.tables));
|
|
181
|
+
const toTables = new Set(Object.keys(toSchema.tables));
|
|
182
|
+
for (const table of [...fromTables].filter((name) => !toTables.has(name)).sort()) {
|
|
183
|
+
steps.push({ kind: 'table.remove', table });
|
|
184
|
+
}
|
|
185
|
+
for (const table of [...toTables].filter((name) => !fromTables.has(name)).sort()) {
|
|
186
|
+
steps.push({ kind: 'table.add', table });
|
|
187
|
+
}
|
|
188
|
+
for (const table of [...fromTables].filter((name) => toTables.has(name)).sort()) {
|
|
189
|
+
steps.push(...tableSteps(table, fromSchema.tables[table], toSchema.tables[table]));
|
|
190
|
+
}
|
|
191
|
+
const classification = steps.every(isCompatible) ? 'compatible' : 'incompatible';
|
|
192
|
+
return { protocol: SCHEMA_PROTOCOL, application, from, to, classification, steps,
|
|
193
|
+
requiresReset: classification === 'incompatible' };
|
|
194
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ApplicationManifestV0, type NormalizedApplicationManifestV0 } from './types.js';
|
|
3
|
+
/** The normalized database fragment embedded in artifacts and review metadata. */
|
|
4
|
+
export declare const normalizedDatabaseSchema: z.ZodObject<{
|
|
5
|
+
version: z.ZodNumber;
|
|
6
|
+
tables: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
7
|
+
fields: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
8
|
+
type: z.ZodEnum<{
|
|
9
|
+
string: "string";
|
|
10
|
+
number: "number";
|
|
11
|
+
boolean: "boolean";
|
|
12
|
+
datetime: "datetime";
|
|
13
|
+
bytes: "bytes";
|
|
14
|
+
json: "json";
|
|
15
|
+
ref: "ref";
|
|
16
|
+
}>;
|
|
17
|
+
optional: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
maxLength: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
|
+
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
22
|
+
collate: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
binary: "binary";
|
|
24
|
+
nocase: "nocase";
|
|
25
|
+
rtrim: "rtrim";
|
|
26
|
+
}>>;
|
|
27
|
+
table: z.ZodOptional<z.ZodString>;
|
|
28
|
+
onDelete: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
noAction: "noAction";
|
|
30
|
+
restrict: "restrict";
|
|
31
|
+
cascade: "cascade";
|
|
32
|
+
setNull: "setNull";
|
|
33
|
+
setDefault: "setDefault";
|
|
34
|
+
}>>;
|
|
35
|
+
onUpdate: z.ZodOptional<z.ZodEnum<{
|
|
36
|
+
noAction: "noAction";
|
|
37
|
+
restrict: "restrict";
|
|
38
|
+
cascade: "cascade";
|
|
39
|
+
setNull: "setNull";
|
|
40
|
+
setDefault: "setDefault";
|
|
41
|
+
}>>;
|
|
42
|
+
generated: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
expression: z.ZodString;
|
|
44
|
+
stored: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
}, z.core.$strict>>;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
indexes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
48
|
+
columns: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
49
|
+
column: z.ZodString;
|
|
50
|
+
collate: z.ZodOptional<z.ZodEnum<{
|
|
51
|
+
binary: "binary";
|
|
52
|
+
nocase: "nocase";
|
|
53
|
+
rtrim: "rtrim";
|
|
54
|
+
}>>;
|
|
55
|
+
desc: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
57
|
+
expression: z.ZodString;
|
|
58
|
+
desc: z.ZodOptional<z.ZodBoolean>;
|
|
59
|
+
}, z.core.$strict>]>>;
|
|
60
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
where: z.ZodOptional<z.ZodString>;
|
|
62
|
+
}, z.core.$strict>]>>>;
|
|
63
|
+
unique: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodString>>>;
|
|
64
|
+
checks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
65
|
+
}, z.core.$strict>>;
|
|
66
|
+
}, z.core.$strict>;
|
|
67
|
+
export declare const applicationManifestSchema: z.ZodObject<{
|
|
68
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
69
|
+
format: z.ZodLiteral<"xeer.application-source.v0">;
|
|
70
|
+
name: z.ZodString;
|
|
71
|
+
entrypoints: z.ZodObject<{
|
|
72
|
+
client: z.ZodString;
|
|
73
|
+
server: z.ZodString;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
app: z.ZodOptional<z.ZodObject<{
|
|
76
|
+
spa: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
+
title: z.ZodOptional<z.ZodString>;
|
|
78
|
+
language: z.ZodOptional<z.ZodString>;
|
|
79
|
+
description: z.ZodOptional<z.ZodString>;
|
|
80
|
+
favicon: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strict>>;
|
|
82
|
+
database: z.ZodOptional<z.ZodObject<{
|
|
83
|
+
version: z.ZodOptional<z.ZodNumber>;
|
|
84
|
+
tables: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
85
|
+
fields: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
86
|
+
type: z.ZodEnum<{
|
|
87
|
+
string: "string";
|
|
88
|
+
number: "number";
|
|
89
|
+
boolean: "boolean";
|
|
90
|
+
datetime: "datetime";
|
|
91
|
+
bytes: "bytes";
|
|
92
|
+
json: "json";
|
|
93
|
+
ref: "ref";
|
|
94
|
+
}>;
|
|
95
|
+
optional: z.ZodOptional<z.ZodBoolean>;
|
|
96
|
+
maxLength: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
99
|
+
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
100
|
+
collate: z.ZodOptional<z.ZodEnum<{
|
|
101
|
+
binary: "binary";
|
|
102
|
+
nocase: "nocase";
|
|
103
|
+
rtrim: "rtrim";
|
|
104
|
+
}>>;
|
|
105
|
+
table: z.ZodOptional<z.ZodString>;
|
|
106
|
+
onDelete: z.ZodOptional<z.ZodEnum<{
|
|
107
|
+
noAction: "noAction";
|
|
108
|
+
restrict: "restrict";
|
|
109
|
+
cascade: "cascade";
|
|
110
|
+
setNull: "setNull";
|
|
111
|
+
setDefault: "setDefault";
|
|
112
|
+
}>>;
|
|
113
|
+
onUpdate: z.ZodOptional<z.ZodEnum<{
|
|
114
|
+
noAction: "noAction";
|
|
115
|
+
restrict: "restrict";
|
|
116
|
+
cascade: "cascade";
|
|
117
|
+
setNull: "setNull";
|
|
118
|
+
setDefault: "setDefault";
|
|
119
|
+
}>>;
|
|
120
|
+
generated: z.ZodOptional<z.ZodObject<{
|
|
121
|
+
expression: z.ZodString;
|
|
122
|
+
stored: z.ZodOptional<z.ZodBoolean>;
|
|
123
|
+
}, z.core.$strict>>;
|
|
124
|
+
}, z.core.$strict>>;
|
|
125
|
+
indexes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
126
|
+
columns: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
127
|
+
column: z.ZodString;
|
|
128
|
+
collate: z.ZodOptional<z.ZodEnum<{
|
|
129
|
+
binary: "binary";
|
|
130
|
+
nocase: "nocase";
|
|
131
|
+
rtrim: "rtrim";
|
|
132
|
+
}>>;
|
|
133
|
+
desc: z.ZodOptional<z.ZodBoolean>;
|
|
134
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
135
|
+
expression: z.ZodString;
|
|
136
|
+
desc: z.ZodOptional<z.ZodBoolean>;
|
|
137
|
+
}, z.core.$strict>]>>;
|
|
138
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
139
|
+
where: z.ZodOptional<z.ZodString>;
|
|
140
|
+
}, z.core.$strict>]>>>;
|
|
141
|
+
unique: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodString>>>;
|
|
142
|
+
checks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
143
|
+
}, z.core.$strict>>;
|
|
144
|
+
}, z.core.$strict>>;
|
|
145
|
+
storage: z.ZodOptional<z.ZodObject<{
|
|
146
|
+
maxObjectBytes: z.ZodOptional<z.ZodNumber>;
|
|
147
|
+
readBytes: z.ZodOptional<z.ZodNumber>;
|
|
148
|
+
writeBytes: z.ZodOptional<z.ZodNumber>;
|
|
149
|
+
}, z.core.$strict>>;
|
|
150
|
+
capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
151
|
+
database: "database";
|
|
152
|
+
storage: "storage";
|
|
153
|
+
}>>>;
|
|
154
|
+
budgets: z.ZodOptional<z.ZodObject<{
|
|
155
|
+
queryRows: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
mutationWrites: z.ZodOptional<z.ZodNumber>;
|
|
157
|
+
requestBytes: z.ZodOptional<z.ZodNumber>;
|
|
158
|
+
responseBytes: z.ZodOptional<z.ZodNumber>;
|
|
159
|
+
liveConnections: z.ZodOptional<z.ZodNumber>;
|
|
160
|
+
}, z.core.$strict>>;
|
|
161
|
+
}, z.core.$strict>;
|
|
162
|
+
/** Stable editor-facing schema location emitted into every scaffolded `xeer.app.json`. */
|
|
163
|
+
export declare const APPLICATION_MANIFEST_SCHEMA_URL: "https://docs.xeer.run/application-v0.schema.json";
|
|
164
|
+
export declare const applicationManifestJsonSchema: Readonly<Record<string, unknown>>;
|
|
165
|
+
export declare function parseApplicationManifest(value: unknown): ApplicationManifestV0;
|
|
166
|
+
export declare function normalizeApplicationManifest(manifest: ApplicationManifestV0): NormalizedApplicationManifestV0;
|