@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.
Files changed (54) hide show
  1. package/README.md +4 -1
  2. package/dist/dev-session.d.ts +1 -1
  3. package/dist/network-policy.js +1 -1
  4. package/dist/server.d.ts +1 -1
  5. package/dist/server.js +4 -4
  6. package/dist/test-run.d.ts +1 -1
  7. package/dist/xeer-cli.d.ts +1 -1
  8. package/package.json +8 -5
  9. package/vendor/spec/actions.d.ts +1250 -0
  10. package/vendor/spec/actions.js +805 -0
  11. package/vendor/spec/admin-sql.d.ts +59 -0
  12. package/vendor/spec/admin-sql.js +147 -0
  13. package/vendor/spec/admin.d.ts +110 -0
  14. package/vendor/spec/admin.js +58 -0
  15. package/vendor/spec/canonical.d.ts +3 -0
  16. package/vendor/spec/canonical.js +36 -0
  17. package/vendor/spec/diagnostics.d.ts +49 -0
  18. package/vendor/spec/diagnostics.js +500 -0
  19. package/vendor/spec/docs.d.ts +21 -0
  20. package/vendor/spec/docs.js +57 -0
  21. package/vendor/spec/events.d.ts +8 -0
  22. package/vendor/spec/events.js +21 -0
  23. package/vendor/spec/identity-keys.d.ts +36 -0
  24. package/vendor/spec/identity-keys.js +72 -0
  25. package/vendor/spec/index.d.ts +20 -0
  26. package/vendor/spec/index.js +20 -0
  27. package/vendor/spec/local-identity.d.ts +69 -0
  28. package/vendor/spec/local-identity.js +132 -0
  29. package/vendor/spec/network-policy.d.ts +16 -0
  30. package/vendor/spec/network-policy.js +50 -0
  31. package/vendor/spec/public-assets.d.ts +153 -0
  32. package/vendor/spec/public-assets.js +166 -0
  33. package/vendor/spec/review.d.ts +120 -0
  34. package/vendor/spec/review.js +226 -0
  35. package/vendor/spec/route.d.ts +43 -0
  36. package/vendor/spec/route.js +87 -0
  37. package/vendor/spec/schema-lifecycle.d.ts +27 -0
  38. package/vendor/spec/schema-lifecycle.js +146 -0
  39. package/vendor/spec/schema-plan.d.ts +98 -0
  40. package/vendor/spec/schema-plan.js +194 -0
  41. package/vendor/spec/schema.d.ts +166 -0
  42. package/vendor/spec/schema.js +409 -0
  43. package/vendor/spec/sql-expression.d.ts +91 -0
  44. package/vendor/spec/sql-expression.js +650 -0
  45. package/vendor/spec/state-export.d.ts +143 -0
  46. package/vendor/spec/state-export.js +341 -0
  47. package/vendor/spec/storage.d.ts +61 -0
  48. package/vendor/spec/storage.js +120 -0
  49. package/vendor/spec/table-ddl.d.ts +162 -0
  50. package/vendor/spec/table-ddl.js +508 -0
  51. package/vendor/spec/types.d.ts +275 -0
  52. package/vendor/spec/types.js +11 -0
  53. package/vendor/spec/value.d.ts +22 -0
  54. package/vendor/spec/value.js +72 -0
@@ -0,0 +1,409 @@
1
+ import { z } from 'zod';
2
+ import { CAPABILITIES, SOURCE_FORMAT, } from './types.js';
3
+ import { STORAGE_DEFAULT_READ_BYTES, STORAGE_DEFAULT_WRITE_BYTES, STORAGE_MAX_OBJECT_BYTES, STORAGE_MAX_READ_BYTES, STORAGE_MAX_WRITE_BYTES, } from './storage.js';
4
+ import { SQL_EXPRESSION_MAX_LENGTH } from './sql-expression.js';
5
+ import { databaseDdl, indexColumns, referenceCycle, RESERVED_TABLE_PREFIXES, reservedTableName } from './table-ddl.js';
6
+ const identifier = z.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/, 'must be an identifier');
7
+ const sourcePath = z.string().min(1).refine((value) => !value.includes('\\'), 'must use forward slashes');
8
+ const title = z.string().min(1).max(120)
9
+ .refine((value) => value === value.trim(), 'must not have leading or trailing whitespace')
10
+ .refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'must not contain control characters');
11
+ const language = z.string().max(35)
12
+ .regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/, 'must be a constrained BCP 47 language tag');
13
+ const description = z.string().max(300)
14
+ .refine((value) => value === value.trim(), 'must not have leading or trailing whitespace')
15
+ .refine((value) => !/[\u0000-\u001f\u007f]/.test(value), 'must not contain control characters');
16
+ const favicon = z.string().min(2).max(256)
17
+ .regex(/^\/[A-Za-z0-9._~!$&'()*+,;=:@/-]+$/, 'must be a root-relative public asset path')
18
+ .refine((value) => !value.includes('//'), 'must not contain empty path segments')
19
+ .refine((value) => value.split('/').slice(1).every((segment) => segment !== '.' && segment !== '..'), 'must not contain dot path segments');
20
+ /**
21
+ * A SQL expression, bounded but otherwise unexamined here: what may appear inside one is decided by
22
+ * `sql-expression.ts`, and checking it needs the table's field list, which a field cannot see. The
23
+ * table and manifest refinements below run the real check by generating the DDL.
24
+ */
25
+ const expression = z.string().min(1).max(SQL_EXPRESSION_MAX_LENGTH);
26
+ const collation = z.enum(['binary', 'nocase', 'rtrim']);
27
+ const referentialAction = z.enum(['noAction', 'restrict', 'cascade', 'setNull', 'setDefault']);
28
+ const LOWERCASE_HEX = /^(?:[0-9a-f]{2})*$/;
29
+ const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
30
+ /** The types a `default` is written in, one per field type. */
31
+ const DEFAULT_JS_TYPE = Object.freeze({
32
+ string: 'string', datetime: 'string', json: 'string', bytes: 'string', ref: 'string',
33
+ number: 'number', boolean: 'boolean',
34
+ });
35
+ /**
36
+ * A default has to satisfy the column it defaults, and SQLite will not say so. `CREATE TABLE` accepts
37
+ * a DEFAULT that every CHECK on the column rejects, and only fails once a row actually uses it — so
38
+ * a schema can be applied cleanly and then refuse the first insert that omits the field.
39
+ */
40
+ function refineDefault(field, ctx) {
41
+ const value = field.default;
42
+ if (value === undefined)
43
+ return;
44
+ const issue = (message) => ctx.addIssue({ code: 'custom', path: ['default'], message });
45
+ if (field.generated)
46
+ return issue('is not valid on a generated column, which computes its own value');
47
+ const expected = DEFAULT_JS_TYPE[field.type];
48
+ if (expected !== undefined && typeof value !== expected) {
49
+ return issue(`must be a ${expected} for a ${field.type} field`);
50
+ }
51
+ if (typeof value !== 'string')
52
+ return;
53
+ if (field.type === 'bytes' && !LOWERCASE_HEX.test(value)) {
54
+ return issue('must be an even number of lowercase hex digits for a bytes field');
55
+ }
56
+ if (field.type === 'datetime' && (!ISO_TIMESTAMP.test(value) || Number.isNaN(Date.parse(value)))) {
57
+ return issue('must be an ISO-8601 UTC timestamp for a datetime field');
58
+ }
59
+ if (field.type === 'json') {
60
+ try {
61
+ JSON.parse(value);
62
+ }
63
+ catch {
64
+ return issue('must be valid JSON for a json field');
65
+ }
66
+ }
67
+ if (field.enum !== undefined && !field.enum.includes(value)) {
68
+ return issue('is not one of the declared enum values, so no row could ever hold it');
69
+ }
70
+ if (field.maxLength !== undefined && value.length > field.maxLength) {
71
+ return issue(`is longer than the declared maxLength of ${field.maxLength}`);
72
+ }
73
+ }
74
+ const fieldSchema = z.strictObject({
75
+ type: z.enum(['string', 'number', 'boolean', 'datetime', 'bytes', 'json', 'ref']),
76
+ optional: z.boolean().optional(),
77
+ maxLength: z.number().int().positive().optional(),
78
+ unique: z.boolean().optional(),
79
+ enum: z.array(z.string().max(1_000)).min(1).max(1_000).optional(),
80
+ default: z.union([z.string().max(1_000), z.number(), z.boolean()]).optional(),
81
+ collate: collation.optional(),
82
+ table: identifier.optional(),
83
+ onDelete: referentialAction.optional(),
84
+ onUpdate: referentialAction.optional(),
85
+ generated: z.strictObject({ expression, stored: z.boolean().optional() }).optional(),
86
+ }).superRefine((field, ctx) => {
87
+ const only = (key, types, message) => {
88
+ if (field[key] !== undefined && !types.includes(field.type)) {
89
+ ctx.addIssue({ code: 'custom', path: [key], message });
90
+ }
91
+ };
92
+ only('maxLength', ['string', 'bytes'], 'is only valid for string or bytes fields');
93
+ only('enum', ['string'], 'is only valid for a string field');
94
+ only('collate', ['string'], 'is only valid for a string field');
95
+ only('table', ['ref'], 'is only valid for a ref field');
96
+ only('onDelete', ['ref'], 'is only valid for a ref field');
97
+ only('onUpdate', ['ref'], 'is only valid for a ref field');
98
+ if (field.type === 'ref' && field.table === undefined) {
99
+ ctx.addIssue({ code: 'custom', path: ['table'], message: 'is required: a ref must name the table it references' });
100
+ }
101
+ if (field.enum !== undefined) {
102
+ for (const [position, value] of field.enum.entries()) {
103
+ if (field.enum.indexOf(value) !== position) {
104
+ ctx.addIssue({ code: 'custom', path: ['enum', position], message: `lists ${JSON.stringify(value)} twice` });
105
+ }
106
+ }
107
+ }
108
+ // Both actions are accepted by CREATE TABLE and then fail every delete or update that fires them.
109
+ for (const [key, action] of [['onDelete', field.onDelete], ['onUpdate', field.onUpdate]]) {
110
+ if (action === 'setNull' && field.optional !== true) {
111
+ ctx.addIssue({ code: 'custom', path: [key], message: 'needs the field to be optional, because it writes NULL' });
112
+ }
113
+ if (action === 'setDefault' && field.default === undefined) {
114
+ ctx.addIssue({ code: 'custom', path: [key], message: 'needs the field to declare a default, because it writes one' });
115
+ }
116
+ }
117
+ refineDefault(field, ctx);
118
+ });
119
+ const indexTermSchema = z.union([
120
+ identifier,
121
+ z.strictObject({ column: identifier, collate: collation.optional(), desc: z.boolean().optional() }),
122
+ z.strictObject({ expression, desc: z.boolean().optional() }),
123
+ ]);
124
+ /**
125
+ * An index is a bare list of columns or the full form. The shorthand covers most indexes and reads
126
+ * better than the object it means, so it stays rather than being migrated away from.
127
+ */
128
+ const indexSchema = z.union([
129
+ z.array(identifier).min(1),
130
+ z.strictObject({
131
+ columns: z.array(indexTermSchema).min(1),
132
+ unique: z.boolean().optional(),
133
+ where: expression.optional(),
134
+ }),
135
+ ]);
136
+ const tableSchema = z.strictObject({
137
+ fields: z.record(identifier, fieldSchema),
138
+ indexes: z.record(identifier, indexSchema).optional(),
139
+ unique: z.array(z.array(identifier).min(1)).optional(),
140
+ checks: z.record(identifier, expression).optional(),
141
+ }).superRefine((table, ctx) => {
142
+ for (const [indexName, definition] of Object.entries(table.indexes ?? {})) {
143
+ const seen = new Set();
144
+ for (const [position, field] of indexColumns(definition).entries()) {
145
+ if (!(field in table.fields)) {
146
+ ctx.addIssue({
147
+ code: 'custom',
148
+ path: ['indexes', indexName, position],
149
+ message: `references unknown field ${JSON.stringify(field)}`,
150
+ });
151
+ }
152
+ if (seen.has(field)) {
153
+ ctx.addIssue({
154
+ code: 'custom',
155
+ path: ['indexes', indexName, position],
156
+ message: `duplicates field ${JSON.stringify(field)}`,
157
+ });
158
+ }
159
+ seen.add(field);
160
+ }
161
+ }
162
+ for (const [position, tuple] of (table.unique ?? []).entries()) {
163
+ const seen = new Set();
164
+ for (const [member, field] of tuple.entries()) {
165
+ if (!(field in table.fields)) {
166
+ ctx.addIssue({ code: 'custom', path: ['unique', position, member],
167
+ message: `references unknown field ${JSON.stringify(field)}` });
168
+ }
169
+ if (seen.has(field)) {
170
+ ctx.addIssue({ code: 'custom', path: ['unique', position, member],
171
+ message: `duplicates field ${JSON.stringify(field)}` });
172
+ }
173
+ seen.add(field);
174
+ }
175
+ }
176
+ for (const reserved of ['id', 'createdAt', 'updatedAt']) {
177
+ if (reserved in table.fields) {
178
+ ctx.addIssue({ code: 'custom', path: ['fields', reserved], message: 'is reserved by the runtime' });
179
+ }
180
+ }
181
+ });
182
+ /**
183
+ * The rules a single table cannot check for itself, plus the generator as a backstop.
184
+ *
185
+ * A `ref` names another table, which only the whole schema can confirm; and every expression in the
186
+ * table — a CHECK, a generated column, an index predicate — is only checkable against the field list
187
+ * the generator already has. Rather than restate those rules, this runs `databaseDdl` and reports
188
+ * what it refuses. That makes the two impossible to disagree: a manifest passes `xeer check` exactly
189
+ * when the DDL for it can be generated, which is what `table-ddl.ts` exists to guarantee.
190
+ */
191
+ function refineDatabaseTables(tables, ctx, base) {
192
+ for (const tableName of Object.keys(tables)) {
193
+ // A declared table is created under its own plain name, so `SELECT * FROM posts` is what an
194
+ // operator types. That is only safe if a declaration cannot claim a name the platform already
195
+ // uses: an application table called `xeer_logs` would otherwise shadow the runtime's own.
196
+ if (reservedTableName(tableName)) {
197
+ ctx.addIssue({
198
+ code: 'custom',
199
+ path: [...base, tableName],
200
+ message: `is a reserved table name; ${RESERVED_TABLE_PREFIXES.join(', ')} are reserved for the platform`,
201
+ });
202
+ }
203
+ const fields = tables[tableName].fields ?? {};
204
+ for (const [fieldName, field] of Object.entries(fields)) {
205
+ if (field.type !== 'ref' || field.table === undefined)
206
+ continue;
207
+ if (!Object.hasOwn(tables, field.table)) {
208
+ ctx.addIssue({
209
+ code: 'custom',
210
+ path: [...base, tableName, 'fields', fieldName, 'table'],
211
+ message: `references ${JSON.stringify(field.table)}, which this schema does not declare`,
212
+ });
213
+ }
214
+ }
215
+ }
216
+ // A reference cycle has no order that can drop its tables; see `referenceCycle`.
217
+ const cycle = referenceCycle({ version: 1, tables: tables });
218
+ if (cycle) {
219
+ ctx.addIssue({
220
+ code: 'custom',
221
+ path: [...base, cycle[0]],
222
+ message: `takes part in a reference cycle (${cycle.join(' → ')}); the tables in one cannot be created or dropped in any order`,
223
+ });
224
+ }
225
+ for (const tableName of Object.keys(tables)) {
226
+ try {
227
+ databaseDdl({ version: 1, tables: { [tableName]: tables[tableName] } });
228
+ }
229
+ catch (error) {
230
+ ctx.addIssue({
231
+ code: 'custom',
232
+ path: [...base, tableName],
233
+ message: error instanceof Error ? error.message : String(error),
234
+ });
235
+ }
236
+ }
237
+ }
238
+ /** The normalized database fragment embedded in artifacts and review metadata. */
239
+ export const normalizedDatabaseSchema = z.strictObject({
240
+ version: z.number().int().positive(),
241
+ tables: z.record(identifier, tableSchema),
242
+ }).superRefine((schema, ctx) => refineDatabaseTables(schema.tables, ctx, ['tables']));
243
+ export const applicationManifestSchema = z.strictObject({
244
+ $schema: z.string().url().optional(),
245
+ format: z.literal(SOURCE_FORMAT),
246
+ name: z.string().regex(/^[a-z][a-z0-9-]{1,62}$/, 'must be a lowercase slug between 2 and 63 characters'),
247
+ entrypoints: z.strictObject({
248
+ client: sourcePath,
249
+ server: sourcePath,
250
+ }),
251
+ app: z.strictObject({
252
+ spa: z.boolean().optional(),
253
+ title: title.optional(),
254
+ language: language.optional(),
255
+ description: description.optional(),
256
+ favicon: favicon.optional(),
257
+ }).optional(),
258
+ database: z.strictObject({
259
+ version: z.number().int().positive().optional(),
260
+ tables: z.record(identifier, tableSchema),
261
+ }).optional(),
262
+ /**
263
+ * The `storage` capability's declared limits (#29). Every field is capped at the platform ceiling
264
+ * here rather than clamped silently at call time: a manifest that asks for more than the platform
265
+ * gives is a repairable diagnostic, and an application whose declared limit is a lie is worse than
266
+ * one that will not build.
267
+ */
268
+ storage: z.strictObject({
269
+ maxObjectBytes: z.number().int().positive().max(STORAGE_MAX_OBJECT_BYTES).optional(),
270
+ readBytes: z.number().int().positive().max(STORAGE_MAX_READ_BYTES).optional(),
271
+ writeBytes: z.number().int().positive().max(STORAGE_MAX_WRITE_BYTES).optional(),
272
+ }).optional(),
273
+ capabilities: z.array(z.enum(CAPABILITIES)).optional(),
274
+ budgets: z.strictObject({
275
+ queryRows: z.number().int().positive().max(10_000).optional(),
276
+ mutationWrites: z.number().int().positive().max(1_000).optional(),
277
+ requestBytes: z.number().int().positive().max(4 * 1024 * 1024).optional(),
278
+ responseBytes: z.number().int().positive().max(4 * 1024 * 1024).optional(),
279
+ // Zero is a declaration, not an omission: it says the application wants no server push at all,
280
+ // and the runtime answers `live_disabled` for it rather than a retryable budget refusal. The
281
+ // bound is `min(0)` and not `positive()` so the editor schema publishes `minimum: 0`.
282
+ liveConnections: z.number().int().min(0).max(1_000).optional(),
283
+ }).optional(),
284
+ }).superRefine((manifest, ctx) => {
285
+ // Caught here, at `xeer check` time, rather than as a failure when the schema is applied.
286
+ refineDatabaseTables(manifest.database?.tables ?? {}, ctx, ['database', 'tables']);
287
+ const capabilities = new Set(manifest.capabilities ?? []);
288
+ for (const [index, name] of (manifest.capabilities ?? []).entries()) {
289
+ if ((manifest.capabilities ?? []).indexOf(name) !== index) {
290
+ ctx.addIssue({ code: 'custom', path: ['capabilities', index], message: `is declared twice: ${JSON.stringify(name)}` });
291
+ }
292
+ }
293
+ if (manifest.database && !capabilities.has('database')) {
294
+ ctx.addIssue({ code: 'custom', path: ['capabilities'], message: 'must include "database" when a database is declared' });
295
+ }
296
+ if (!manifest.database && capabilities.has('database')) {
297
+ ctx.addIssue({ code: 'custom', path: ['database'], message: 'must be declared when the database capability is enabled' });
298
+ }
299
+ // The same two-way rule, for the same reason: a config block nobody granted is dead configuration,
300
+ // and a granted capability with no block is a limit set nobody declared. Unlike `database`, whose
301
+ // block carries the whole schema, an empty `"storage": {}` is legitimate — it means "the platform
302
+ // defaults" — so the block must be *present*, not non-empty.
303
+ if (manifest.storage && !capabilities.has('storage')) {
304
+ ctx.addIssue({ code: 'custom', path: ['capabilities'], message: 'must include "storage" when a storage block is declared' });
305
+ }
306
+ if (!manifest.storage && capabilities.has('storage')) {
307
+ ctx.addIssue({ code: 'custom', path: ['storage'],
308
+ message: 'must be declared when the storage capability is enabled; use {} for the platform defaults' });
309
+ }
310
+ });
311
+ /** Stable editor-facing schema location emitted into every scaffolded `xeer.app.json`. */
312
+ export const APPLICATION_MANIFEST_SCHEMA_URL = 'https://docs.xeer.run/application-v0.schema.json';
313
+ /**
314
+ * JSON Schema generated from the same Zod object that validates manifests at runtime. The few
315
+ * cross-field rules below mirror `superRefine` constraints that JSON Schema can express, while
316
+ * compiler diagnostics remain authoritative for rules such as index fields referring to a declared
317
+ * table field.
318
+ */
319
+ const generatedApplicationManifestJsonSchema = z.toJSONSchema(applicationManifestSchema);
320
+ const generatedManifestProperties = generatedApplicationManifestJsonSchema.properties;
321
+ const describeProperty = (name, description) => ({
322
+ ...generatedManifestProperties[name],
323
+ description,
324
+ });
325
+ export const applicationManifestJsonSchema = Object.freeze({
326
+ ...generatedApplicationManifestJsonSchema,
327
+ $id: APPLICATION_MANIFEST_SCHEMA_URL,
328
+ title: 'Xeer application source manifest v0',
329
+ description: 'The complete declarative application manifest read by the Xeer compiler.',
330
+ properties: {
331
+ ...generatedManifestProperties,
332
+ $schema: describeProperty('$schema', 'Editor schema hint. Xeer does not fetch this URL during validation.'),
333
+ format: describeProperty('format', 'Manifest protocol discriminator; exactly xeer.application-source.v0.'),
334
+ name: describeProperty('name', 'Lowercase application slug, between 2 and 63 characters.'),
335
+ entrypoints: describeProperty('entrypoints', 'Project-relative client and server source entrypoints.'),
336
+ app: describeProperty('app', 'Browser metadata and SPA behavior.'),
337
+ database: describeProperty('database', 'Typed application database schema. Requires the database capability.'),
338
+ storage: describeProperty('storage', 'Private object-storage limits. Requires the storage capability.'),
339
+ capabilities: {
340
+ ...describeProperty('capabilities', 'Powers granted to the application. Duplicate names are invalid.'),
341
+ uniqueItems: true,
342
+ },
343
+ budgets: describeProperty('budgets', 'Per-operation resource ceilings, each lowerable from platform defaults.'),
344
+ },
345
+ allOf: [
346
+ {
347
+ if: { required: ['database'] },
348
+ then: { required: ['capabilities'], properties: {
349
+ capabilities: { type: 'array', contains: { const: 'database' } },
350
+ } },
351
+ },
352
+ {
353
+ if: { required: ['capabilities'], properties: {
354
+ capabilities: { type: 'array', contains: { const: 'database' } },
355
+ } },
356
+ then: { required: ['database'] },
357
+ },
358
+ {
359
+ if: { required: ['storage'] },
360
+ then: { required: ['capabilities'], properties: {
361
+ capabilities: { type: 'array', contains: { const: 'storage' } },
362
+ } },
363
+ },
364
+ {
365
+ if: { required: ['capabilities'], properties: {
366
+ capabilities: { type: 'array', contains: { const: 'storage' } },
367
+ } },
368
+ then: { required: ['storage'] },
369
+ },
370
+ ],
371
+ });
372
+ export function parseApplicationManifest(value) {
373
+ return applicationManifestSchema.parse(value);
374
+ }
375
+ export function normalizeApplicationManifest(manifest) {
376
+ return {
377
+ format: SOURCE_FORMAT,
378
+ name: manifest.name,
379
+ entrypoints: { ...manifest.entrypoints },
380
+ app: {
381
+ spa: manifest.app?.spa ?? true,
382
+ title: manifest.app?.title ?? manifest.name,
383
+ language: manifest.app?.language ?? 'en',
384
+ description: manifest.app?.description ?? '',
385
+ favicon: manifest.app?.favicon ?? null,
386
+ },
387
+ database: { version: manifest.database?.version ?? 1, tables: manifest.database?.tables ?? {} },
388
+ // Normalized only when declared. The schema above has already rejected a block without the
389
+ // capability, so `manifest.storage` being present *is* the capability being granted.
390
+ storage: manifest.storage
391
+ ? {
392
+ maxObjectBytes: manifest.storage.maxObjectBytes ?? STORAGE_MAX_OBJECT_BYTES,
393
+ readBytes: manifest.storage.readBytes ?? STORAGE_DEFAULT_READ_BYTES,
394
+ writeBytes: manifest.storage.writeBytes ?? STORAGE_DEFAULT_WRITE_BYTES,
395
+ }
396
+ : null,
397
+ capabilities: [...(manifest.capabilities ?? [])].sort(),
398
+ budgets: {
399
+ queryRows: manifest.budgets?.queryRows ?? 1_000,
400
+ mutationWrites: manifest.budgets?.mutationWrites ?? 100,
401
+ requestBytes: manifest.budgets?.requestBytes ?? 1_048_576,
402
+ responseBytes: manifest.budgets?.responseBytes ?? 1_048_576,
403
+ // Off unless the application asks for it. One browser tab holding a live query pins the
404
+ // application's Durable Object continuously, so a positive default charged every application
405
+ // for a feature most never used. Declare a positive value to turn it on.
406
+ liveConnections: manifest.budgets?.liveConnections ?? 0,
407
+ },
408
+ };
409
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The SQL text primitives every generated statement is built out of, and the one place an
3
+ * author-written expression is allowed to become SQL.
4
+ *
5
+ * A manifest may carry expressions — a table CHECK, a generated column, a partial index's WHERE, an
6
+ * index over `lower(title)`. There is no way to bind those as parameters: they are DDL, not values,
7
+ * so they reach SQLite as text. That makes them the only part of the manifest that could smuggle
8
+ * syntax into a statement, and the reason this module exists is to make "expression" a closed
9
+ * vocabulary rather than a passthrough.
10
+ *
11
+ * The expression is lexed into tokens and re-emitted from them. Nothing survives that the lexer did
12
+ * not recognize, which buys three things at once:
13
+ *
14
+ * - **Escaping.** A column reference is re-emitted through {@link sqlIdentifier} and a string
15
+ * through {@link sqlStringLiteral}, so quoting is structural rather than a rule someone has to
16
+ * remember. There is no path by which raw author text reaches the statement.
17
+ * - **Scope.** Every bare word is either a keyword, an allowlisted function, or a column of *this*
18
+ * table. A statement separator, a comment, a bind parameter, a qualified `other.column`, and the
19
+ * word `SELECT` all have no token, so a subquery cannot be written at all.
20
+ * - **Determinism.** Whitespace and keyword case are normalized on the way out, so the same
21
+ * expression always produces byte-identical DDL.
22
+ *
23
+ * The function allowlist holds only *deterministic* functions. That is not caution: SQLite refuses a
24
+ * non-deterministic function in a CHECK, a generated column, and an index, so `datetime('now')` in
25
+ * any of those positions is an error at CREATE time. Rejecting it here turns a failure when the
26
+ * schema is applied into a diagnostic when it is written.
27
+ */
28
+ /** Every function an expression may call. Deterministic only — see the module comment. */
29
+ export declare const SQL_EXPRESSION_FUNCTIONS: readonly string[];
30
+ /**
31
+ * How many arguments each allowlisted function takes, as `[minimum, maximum]`.
32
+ *
33
+ * SQLite reports a bad count as `wrong number of arguments to function lower()` when the statement
34
+ * is prepared, which for DDL means when the schema is applied rather than when it is written. The
35
+ * table is exhaustive over {@link SQL_EXPRESSION_FUNCTIONS} and a test holds it that way, because a
36
+ * function reachable without an entry would be a function nobody counts.
37
+ *
38
+ * `max` and `min` are the reason this is not merely tidiness. Both are scalar with two or more
39
+ * arguments and *aggregates* with one, and SQLite answers the aggregate form with `misuse of
40
+ * aggregate function max()` in a CHECK, a generated column and an index alike. Requiring two
41
+ * arguments keeps `max(position, 0)` and turns `max(position)` into a diagnostic.
42
+ */
43
+ export declare const SQL_FUNCTION_ARITY: Readonly<Record<string, readonly [number, number]>>;
44
+ /**
45
+ * The longest expression a manifest may declare. An expression is copied verbatim into the schema
46
+ * hash, the DDL, and every diagnostic that quotes it, so it is bounded for the same reason a field
47
+ * name is.
48
+ */
49
+ export declare const SQL_EXPRESSION_MAX_LENGTH = 1000;
50
+ /**
51
+ * The most interior nodes an expression may build.
52
+ *
53
+ * Cloudflare's SQLite refuses an expression tree deeper than 100. Measured in workerd by bisection:
54
+ * a chain of 100 bare terms — that is, 99 `AND` operators — parses, and 101 terms (100 operators)
55
+ * fails with "Expression tree is too large (maximum depth 100)". The limit counts operators rather
56
+ * than terms. The character cap does not stand in for it: 1000 characters buys well over 100 terms,
57
+ * so without a node budget a manifest could pass `xeer check` and then fail to apply.
58
+ *
59
+ * Every interior node of the tree is built by exactly one operator, function call, or grammar
60
+ * keyword, and a root-to-leaf path visits each of its nodes once — so depth never exceeds the count
61
+ * of those tokens. Counting them is therefore a sound upper bound, and one that stays cheap: it
62
+ * needs no parser, only the tokens the lexer already produced.
63
+ *
64
+ * What is deliberately *not* counted is anything that costs no depth. An `IN` list is a single node
65
+ * however many values it holds — 2000 of them parse, and 1000 of them still reject a non-member —
66
+ * so a long enum is free.
67
+ * Parentheses are free too: workerd parsed 200 nested ones without complaint.
68
+ */
69
+ export declare const SQL_EXPRESSION_MAX_NODES = 90;
70
+ export declare class SqlExpressionError extends Error {
71
+ constructor(message: string);
72
+ }
73
+ /** `title` → `"title"`. Doubling the quote is the whole of SQLite's identifier escaping. */
74
+ export declare function sqlIdentifier(name: string): string;
75
+ /** `O'Brien` → `'O''Brien'`. Doubling the apostrophe is the whole of SQLite's string escaping. */
76
+ export declare function sqlStringLiteral(value: string): string;
77
+ /** `00ff` → `X'00ff'`. Lowercase hex only, so one byte sequence has exactly one spelling. */
78
+ export declare function sqlBlobLiteral(hex: string): string;
79
+ /** SQL has no spelling for NaN or an infinity, so a number that is not finite has no literal. */
80
+ export declare function sqlNumberLiteral(value: number): string;
81
+ export interface CompiledSqlExpressionV0 {
82
+ /** The canonical SQL text, safe to embed in a statement. */
83
+ readonly sql: string;
84
+ /** Every column the expression reads, sorted and deduplicated. */
85
+ readonly columns: readonly string[];
86
+ }
87
+ /**
88
+ * Validate an author-written expression against the columns it is allowed to read, and return the
89
+ * canonical SQL for it. Throws {@link SqlExpressionError} with a message naming the rule it broke.
90
+ */
91
+ export declare function compileSqlExpression(source: string, columns: readonly string[]): CompiledSqlExpressionV0;