primitive-admin 1.0.54 → 1.0.55
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/bin/primitive.js +2 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/apps.js +54 -2
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/databases.js +31 -10
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +77 -0
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/settings.d.ts +15 -0
- package/dist/src/commands/settings.js +102 -0
- package/dist/src/commands/settings.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +105 -0
- package/dist/src/commands/sync-app-settings.js +339 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +34 -19
- package/dist/src/commands/sync.js +373 -200
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/workflows.js +32 -10
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +2 -0
- package/dist/src/lib/api-client.js +6 -0
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +108 -0
- package/dist/src/lib/app-settings-descriptor.js +250 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +15 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +27 -10
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +11 -83
- package/dist/src/lib/db-codegen/dbGenerator.js +93 -556
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +15 -7
- package/dist/src/lib/db-codegen/dbNaming.js +25 -13
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +55 -4
- package/dist/src/lib/db-codegen/dbTemplates.js +169 -40
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +517 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +101 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +200 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +68 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +380 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +84 -0
- package/dist/src/lib/swift-codegen/generator.js +178 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +72 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +644 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +225 -0
- package/dist/src/lib/sync-resource-types.js +394 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +18 -5
- package/dist/src/lib/workflow-codegen/generator.js +116 -41
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +85 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +71 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/package.json +4 -2
|
@@ -6,605 +6,142 @@
|
|
|
6
6
|
* then either (a) writes them, or (b) compares them against on-disk for
|
|
7
7
|
* `--check`. No commander dependency, no `process.exit`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* and, where the shape is derivable, the op's parsed `definition`: a query op's
|
|
18
|
-
* `projection` (#1439), a mutation op's step kinds (#1438), and a pipeline op's
|
|
19
|
-
* `return` step (#1437). This file resolves those into the small descriptors
|
|
20
|
-
* the templates consume; the templates stay pure string-emitters.
|
|
9
|
+
* The op/record RESOLUTION (parse, schema load, required-ness inference, op
|
|
10
|
+
* params, and per-op result descriptors) lives in the shared, naming-neutral
|
|
11
|
+
* `describeDbType` IR (`dbTypeIR.ts`) — the SAME source the Swift renderer
|
|
12
|
+
* (`swift-codegen/dbGenerator.ts`) consumes (issue #1547 binding 5). This file
|
|
13
|
+
* is now purely the TS *rendering* half: it applies TS identifier naming
|
|
14
|
+
* (`resolveRecordInterfaceName`, `opParamsInterfaceName`, …) to that IR and
|
|
15
|
+
* emits `*.generated.ts` via the pure `dbTemplates` string-emitters. The
|
|
16
|
+
* emitted bytes are unchanged from the former inline implementation.
|
|
21
17
|
*/
|
|
22
18
|
import * as path from "path";
|
|
23
|
-
import { parseConfigToml, stringifyConfigToml } from "../config-toml.js";
|
|
24
|
-
import { loadSchemaFromTomlString } from "js-bao";
|
|
25
|
-
import { parseDatabaseTypeToml } from "../../commands/sync.js";
|
|
26
19
|
import { writeOrCheckGeneratedFiles, } from "../codegen-shared/generatedFiles.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
20
|
+
import { describeDbType, } from "./dbTypeIR.js";
|
|
21
|
+
import { resolveRecordInterfaceName, opParamsInterfaceName, opResultAliasName, opsFactoryName, subscriptionsFactoryName, objectPropertyKey, } from "./dbNaming.js";
|
|
22
|
+
import { renderHeader, renderRecordInterface, renderOpParamsInterface, renderOpResultAlias, renderFactories, RESULT_ALIASES_BLOCK, } from "./dbTemplates.js";
|
|
23
|
+
export { inferArrayParamElementTypes, inferRequiredRecordFields, } from "./dbTypeIR.js";
|
|
31
24
|
const GENERATED_FILE_SUFFIX = ".generated.ts";
|
|
32
25
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* array-element inference must not rewrite it to an array type in the generated
|
|
36
|
-
* interface. Keep in step with the runtime's scalar coercion list
|
|
37
|
-
* (`MODEL_PARAM_SCALAR_TYPES` in `database-operations-controller.ts`).
|
|
26
|
+
* Map the naming-neutral pipeline-return IR to the interface-named descriptor
|
|
27
|
+
* the TS `renderOpResultAlias` template consumes.
|
|
38
28
|
*/
|
|
39
|
-
|
|
29
|
+
function mapPipelineReturn(ir, recordInterfaceByModel) {
|
|
30
|
+
switch (ir.kind) {
|
|
31
|
+
case "query":
|
|
32
|
+
return {
|
|
33
|
+
kind: "query",
|
|
34
|
+
recordInterfaceName: ir.modelName
|
|
35
|
+
? recordInterfaceByModel[ir.modelName] ?? null
|
|
36
|
+
: null,
|
|
37
|
+
projection: ir.projection,
|
|
38
|
+
};
|
|
39
|
+
case "count":
|
|
40
|
+
return { kind: "count" };
|
|
41
|
+
case "aggregate":
|
|
42
|
+
return { kind: "aggregate" };
|
|
43
|
+
case "opaque":
|
|
44
|
+
default:
|
|
45
|
+
return { kind: "opaque" };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* Render the in-memory `.generated.ts` content for a single database-type
|
|
42
50
|
* TOML. Exported so tests can assert on the rendered string directly.
|
|
43
51
|
*/
|
|
44
52
|
export function renderDbTypeFile(input) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
// the [models.*] subtree through the shared js-bao loader (same path the
|
|
49
|
-
// server uses to validate the schema).
|
|
50
|
-
const tomlData = parseConfigToml(tomlContent);
|
|
51
|
-
const { typeConfig, operations } = parseDatabaseTypeToml(tomlData);
|
|
52
|
-
// 1. Record interfaces (read shape) from [models.*].
|
|
53
|
-
const schemas = tomlData.models
|
|
54
|
-
? loadSchemaFromTomlString(stringifyConfigToml({ models: tomlData.models }), {
|
|
55
|
-
strict: false,
|
|
56
|
-
})
|
|
57
|
-
: [];
|
|
58
|
-
const stampedFields = collectStampedFields(typeConfig);
|
|
59
|
-
// model name → set of record fields the operation params prove are always
|
|
60
|
-
// supplied at create time (every `save` op that sets the field binds it to a
|
|
61
|
-
// `required: true` param). OR'd with the schema-level `required` flag below;
|
|
62
|
-
// inference only ever ADDS requiredness, never removes it. (#842)
|
|
63
|
-
const inferredRequired = inferRequiredRecordFields(operations);
|
|
64
|
-
// model name → resolved record interface name (used by query result aliases).
|
|
53
|
+
const ir = describeDbType(input);
|
|
54
|
+
// model name → resolved record interface name (used by query result aliases),
|
|
55
|
+
// with the same collision guard the inline implementation had.
|
|
65
56
|
const recordInterfaceByModel = {};
|
|
66
|
-
// model name → the field names that appear on its record interface (declared
|
|
67
|
-
// fields + server-stamped fields). Used to guard projection `Pick`
|
|
68
|
-
// against keys that are not members of the interface (#1439).
|
|
69
|
-
const recordFieldsByModel = new Map();
|
|
70
|
-
// model name → declared field name → field type. Feeds the `$in`/`$nin`
|
|
71
|
-
// array-param element-type inference (#1488) below.
|
|
72
|
-
const fieldTypesByModel = new Map();
|
|
73
|
-
for (const schema of schemas) {
|
|
74
|
-
const ftypes = new Map();
|
|
75
|
-
for (const [fname, fopts] of Object.entries(schema.fields)) {
|
|
76
|
-
const t = fopts?.type;
|
|
77
|
-
if (typeof t === "string")
|
|
78
|
-
ftypes.set(fname, t);
|
|
79
|
-
}
|
|
80
|
-
fieldTypesByModel.set(schema.name, ftypes);
|
|
81
|
-
}
|
|
82
57
|
const seenInterfaceNames = new Map(); // interfaceName → modelName
|
|
83
|
-
for (const
|
|
84
|
-
const ifaceName = resolveRecordInterfaceName(
|
|
58
|
+
for (const rec of ir.records) {
|
|
59
|
+
const ifaceName = resolveRecordInterfaceName(rec.modelName, rec.className);
|
|
85
60
|
const prior = seenInterfaceNames.get(ifaceName);
|
|
86
|
-
if (prior !== undefined && prior !==
|
|
61
|
+
if (prior !== undefined && prior !== rec.modelName) {
|
|
87
62
|
throw new Error(`Record-interface name collision: models "${prior}" and ` +
|
|
88
|
-
`"${
|
|
63
|
+
`"${rec.modelName}" both produce interface "${ifaceName}". Add a ` +
|
|
89
64
|
`class_name override to one of them.`);
|
|
90
65
|
}
|
|
91
|
-
seenInterfaceNames.set(ifaceName,
|
|
92
|
-
recordInterfaceByModel[
|
|
93
|
-
const memberNames = new Set(Object.keys(schema.fields));
|
|
94
|
-
for (const stamped of stampedFields)
|
|
95
|
-
memberNames.add(stamped);
|
|
96
|
-
recordFieldsByModel.set(schema.name, memberNames);
|
|
66
|
+
seenInterfaceNames.set(ifaceName, rec.modelName);
|
|
67
|
+
recordInterfaceByModel[rec.modelName] = ifaceName;
|
|
97
68
|
}
|
|
98
69
|
const blocks = [];
|
|
99
|
-
blocks.push(renderHeader(fingerprint));
|
|
70
|
+
blocks.push(renderHeader(ir.fingerprint));
|
|
100
71
|
blocks.push(RESULT_ALIASES_BLOCK.trimEnd());
|
|
101
72
|
// Record interfaces, sorted by interface name for deterministic output.
|
|
102
|
-
const
|
|
103
|
-
for (const
|
|
104
|
-
const inferredForModel = inferredRequired.get(schema.name)?.fields;
|
|
105
|
-
const fields = Object.entries(schema.fields).map(([name, opts]) => ({
|
|
106
|
-
name,
|
|
107
|
-
type: opts.type,
|
|
108
|
-
required: opts.required === true ||
|
|
109
|
-
(opts.autoAssign === true && name === "id") ||
|
|
110
|
-
inferredForModel?.has(name) === true,
|
|
111
|
-
// Allowed-value set for string fields → string-literal union (#843).
|
|
112
|
-
// Threaded through the shared loader's FieldOptions.enum; validated
|
|
113
|
-
// (non-empty, all-string, string-only) by parseFieldOptions upstream.
|
|
114
|
-
enum: opts.enum,
|
|
115
|
-
}));
|
|
73
|
+
const sortedRecords = [...ir.records].sort((a, b) => recordInterfaceByModel[a.modelName].localeCompare(recordInterfaceByModel[b.modelName]));
|
|
74
|
+
for (const rec of sortedRecords) {
|
|
116
75
|
blocks.push(renderRecordInterface({
|
|
117
|
-
interfaceName: recordInterfaceByModel[
|
|
118
|
-
modelName:
|
|
119
|
-
fields,
|
|
120
|
-
stampedFields,
|
|
76
|
+
interfaceName: recordInterfaceByModel[rec.modelName],
|
|
77
|
+
modelName: rec.modelName,
|
|
78
|
+
fields: rec.fields,
|
|
79
|
+
stampedFields: ir.stampedFields,
|
|
121
80
|
}));
|
|
122
81
|
}
|
|
123
|
-
// Operation interfaces + result aliases, sorted by op name.
|
|
124
|
-
|
|
125
|
-
.filter((op) => op && typeof op.name === "string")
|
|
126
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
-
// Descriptors for the typed-ops factory (#1441), collected in the same
|
|
128
|
-
// name-sorted order so the factory's methods are deterministic.
|
|
82
|
+
// Operation interfaces + result aliases, already sorted by op name in the IR.
|
|
83
|
+
// Descriptors for the typed-ops factory (#1441), collected in the same order.
|
|
129
84
|
const factoryOps = [];
|
|
130
|
-
for (const op of
|
|
131
|
-
const inferredArrays = inferArrayParamElementTypes(op, fieldTypesByModel);
|
|
132
|
-
const params = paramsToFields(op.params, inferredArrays);
|
|
85
|
+
for (const op of ir.ops) {
|
|
133
86
|
blocks.push(renderOpParamsInterface({
|
|
134
|
-
interfaceName: opParamsInterfaceName(op.
|
|
135
|
-
opName: op.
|
|
136
|
-
params,
|
|
87
|
+
interfaceName: opParamsInterfaceName(op.opName),
|
|
88
|
+
opName: op.opName,
|
|
89
|
+
params: op.params,
|
|
137
90
|
}));
|
|
138
91
|
factoryOps.push({
|
|
139
|
-
propertyKey:
|
|
140
|
-
opName: op.
|
|
141
|
-
paramsInterfaceName: opParamsInterfaceName(op.
|
|
142
|
-
resultAliasName: opResultAliasName(op.
|
|
143
|
-
hasParams:
|
|
92
|
+
propertyKey: objectPropertyKey(op.opName),
|
|
93
|
+
opName: op.opName,
|
|
94
|
+
paramsInterfaceName: opParamsInterfaceName(op.opName),
|
|
95
|
+
resultAliasName: opResultAliasName(op.opName),
|
|
96
|
+
hasParams: op.hasParams,
|
|
144
97
|
});
|
|
145
|
-
const recordInterfaceName = op.
|
|
146
|
-
? recordInterfaceByModel[op.
|
|
98
|
+
const recordInterfaceName = op.resultModelName
|
|
99
|
+
? recordInterfaceByModel[op.resultModelName] ?? null
|
|
147
100
|
: null;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
? classifyProjection(op.definition?.projection, (op.modelName && recordFieldsByModel.get(op.modelName)) ||
|
|
151
|
-
new Set())
|
|
152
|
-
: undefined;
|
|
153
|
-
// Pipeline ops: resolve the `return`-step result shape (#1437).
|
|
154
|
-
const pipelineReturn = op.type === "pipeline"
|
|
155
|
-
? resolvePipelineReturn(op.definition, recordInterfaceByModel, recordFieldsByModel)
|
|
101
|
+
const pipelineReturn = op.pipelineReturn
|
|
102
|
+
? mapPipelineReturn(op.pipelineReturn, recordInterfaceByModel)
|
|
156
103
|
: undefined;
|
|
157
104
|
blocks.push(renderOpResultAlias({
|
|
158
|
-
aliasName: opResultAliasName(op.
|
|
159
|
-
opName: op.
|
|
160
|
-
opType: op.
|
|
105
|
+
aliasName: opResultAliasName(op.opName),
|
|
106
|
+
opName: op.opName,
|
|
107
|
+
opType: op.opType,
|
|
161
108
|
recordInterfaceName,
|
|
162
|
-
projection,
|
|
109
|
+
projection: op.projection,
|
|
163
110
|
pipelineReturn,
|
|
164
111
|
}));
|
|
165
112
|
}
|
|
166
|
-
// Typed-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
113
|
+
// Typed-subscriptions descriptors (#1544 Phase 2): map each subscription's
|
|
114
|
+
// model to its record interface (or a `Pick` when it declares `select`). The
|
|
115
|
+
// IR already validated that every model resolves and every select field is a
|
|
116
|
+
// known member, so this mapping never fails.
|
|
117
|
+
const factorySubscriptions = ir.subscriptions.map((sub) => {
|
|
118
|
+
const interfaceName = recordInterfaceByModel[sub.modelName];
|
|
119
|
+
const eventType = sub.select && sub.select.length > 0
|
|
120
|
+
? `Pick<${interfaceName}, ${sub.select
|
|
121
|
+
.map((f) => JSON.stringify(f))
|
|
122
|
+
.join(" | ")}>`
|
|
123
|
+
: interfaceName;
|
|
124
|
+
return {
|
|
125
|
+
propertyKey: objectPropertyKey(sub.subscriptionKey),
|
|
126
|
+
subscriptionKey: sub.subscriptionKey,
|
|
127
|
+
eventType,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
// Typed-ops factory (#1441) + typed-subscriptions factory (#1544 Phase 2),
|
|
131
|
+
// sharing one structural client interface. Omitted entirely for a type with
|
|
132
|
+
// neither ops nor subscriptions.
|
|
133
|
+
if (factoryOps.length > 0 || factorySubscriptions.length > 0) {
|
|
134
|
+
blocks.push(renderFactories({
|
|
171
135
|
databaseType: input.databaseType,
|
|
136
|
+
opsFactoryName: opsFactoryName(input.databaseType),
|
|
137
|
+
subscriptionsFactoryName: subscriptionsFactoryName(input.databaseType),
|
|
172
138
|
ops: factoryOps,
|
|
139
|
+
subscriptions: factorySubscriptions,
|
|
173
140
|
}));
|
|
174
141
|
}
|
|
175
142
|
// Single trailing newline; blocks separated by a blank line.
|
|
176
143
|
return blocks.join("\n\n") + "\n";
|
|
177
144
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Classify a query op's static `projection` into how it narrows the record type
|
|
180
|
-
* (#1439). Kept private to the generator: the returned `{ kind, fields }` shape
|
|
181
|
-
* is what the template needs, and the field-guard / `id`-folding / sorting live
|
|
182
|
-
* here so the template stays a pure string-emitter.
|
|
183
|
-
*
|
|
184
|
-
* `recordFieldNames` is the set of members on the record interface (declared +
|
|
185
|
-
* stamped). A projected key that is not a member (a nested `"a.b"` path, or a
|
|
186
|
-
* field absent from the schema) makes the whole projection fall back to
|
|
187
|
-
* `Partial<M>` rather than emit an invalid `Pick`. Reused for a pipeline
|
|
188
|
-
* op's returning `query` step (#1437).
|
|
189
|
-
*
|
|
190
|
-
* Rules (projection values are validated server-side to be `1` or `0`, mixing
|
|
191
|
-
* rejected — codegen runs pre-push, so it still guards):
|
|
192
|
-
* - absent / not an object / empty `{}` → `full` (whole record `M`).
|
|
193
|
-
* - all `1` (inclusion), keys all known → `pick` (with `id` folded in,
|
|
194
|
-
* since inclusion always retains
|
|
195
|
-
* `id` at runtime).
|
|
196
|
-
* - all `0` (exclusion), keys all known → `partial`. The DO runtime does
|
|
197
|
-
* not strip excluded fields yet, so
|
|
198
|
-
* an `Omit` would hide fields that
|
|
199
|
-
* are still returned (#1439/Codex-P2).
|
|
200
|
-
* - mixed / dynamic / non-`1|0` / unknown key → `partial` (honest fallback).
|
|
201
|
-
*/
|
|
202
|
-
function classifyProjection(projection, recordFieldNames) {
|
|
203
|
-
if (!projection ||
|
|
204
|
-
typeof projection !== "object" ||
|
|
205
|
-
Array.isArray(projection)) {
|
|
206
|
-
return { kind: "full" };
|
|
207
|
-
}
|
|
208
|
-
const entries = Object.entries(projection);
|
|
209
|
-
if (entries.length === 0)
|
|
210
|
-
return { kind: "full" };
|
|
211
|
-
let allInclude = true;
|
|
212
|
-
let allExclude = true;
|
|
213
|
-
const included = [];
|
|
214
|
-
const excluded = [];
|
|
215
|
-
for (const [field, val] of entries) {
|
|
216
|
-
if (val === 1) {
|
|
217
|
-
included.push(field);
|
|
218
|
-
allExclude = false;
|
|
219
|
-
}
|
|
220
|
-
else if (val === 0) {
|
|
221
|
-
excluded.push(field);
|
|
222
|
-
allInclude = false;
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
// A dynamic (`$params.x`) or otherwise non-`1|0` value — can't resolve
|
|
226
|
-
// statically. Fall back rather than emit garbage.
|
|
227
|
-
allInclude = false;
|
|
228
|
-
allExclude = false;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (allInclude && included.length > 0) {
|
|
232
|
-
if (!included.every((f) => recordFieldNames.has(f)))
|
|
233
|
-
return { kind: "partial" };
|
|
234
|
-
// Inclusion always retains `id` at runtime (the DO unshifts it), so fold it
|
|
235
|
-
// into the union when the model has an `id` field.
|
|
236
|
-
const fields = new Set(included);
|
|
237
|
-
if (recordFieldNames.has("id"))
|
|
238
|
-
fields.add("id");
|
|
239
|
-
return { kind: "pick", fields: [...fields].sort() };
|
|
240
|
-
}
|
|
241
|
-
if (allExclude && excluded.length > 0) {
|
|
242
|
-
// Exclusion projection (e.g. `{ secret: 0 }`). The DO query runtime does
|
|
243
|
-
// NOT actually strip excluded fields today: `JsonQueryTranslator` selects
|
|
244
|
-
// `_data` for excludes and `_handleQuery` merges the full row via
|
|
245
|
-
// `_parseRow`, so the excluded field is still present in the response.
|
|
246
|
-
// Emitting `Omit<M, ...>` would hide fields that ARE returned and break
|
|
247
|
-
// existing TS callers after regeneration, so type these as `Partial<M>`
|
|
248
|
-
// until the runtime filters them (#1439 / Codex-P2).
|
|
249
|
-
return { kind: "partial" };
|
|
250
|
-
}
|
|
251
|
-
return { kind: "partial" };
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Resolve a pipeline op's `return`-step result shape (#1437). The runtime
|
|
255
|
-
* flattens a returning pipeline to the returned step's shape, so codegen types
|
|
256
|
-
* it as that step's result. `return: "all"`, no `return`, or any step the
|
|
257
|
-
* generator can't resolve fall back to `opaque` → the generic `PipelineResult`.
|
|
258
|
-
* Never throws — one odd op must not break codegen for the whole file.
|
|
259
|
-
*/
|
|
260
|
-
function resolvePipelineReturn(definition, recordInterfaceByModel, recordFieldsByModel) {
|
|
261
|
-
if (!definition || typeof definition !== "object")
|
|
262
|
-
return { kind: "opaque" };
|
|
263
|
-
const def = definition;
|
|
264
|
-
const ret = def.return ?? "all";
|
|
265
|
-
if (ret === "all" || typeof ret !== "string")
|
|
266
|
-
return { kind: "opaque" };
|
|
267
|
-
if (!Array.isArray(def.steps))
|
|
268
|
-
return { kind: "opaque" };
|
|
269
|
-
const step = def.steps.find((s) => s && typeof s === "object" && s.name === ret);
|
|
270
|
-
if (!step)
|
|
271
|
-
return { kind: "opaque" };
|
|
272
|
-
if (step.type === "query") {
|
|
273
|
-
const modelName = typeof step.modelName === "string" ? step.modelName : undefined;
|
|
274
|
-
const recordInterfaceName = modelName
|
|
275
|
-
? recordInterfaceByModel[modelName] ?? null
|
|
276
|
-
: null;
|
|
277
|
-
const fieldNames = (modelName && recordFieldsByModel.get(modelName)) || new Set();
|
|
278
|
-
const projection = classifyProjection(step.projection, fieldNames);
|
|
279
|
-
return { kind: "query", recordInterfaceName, projection };
|
|
280
|
-
}
|
|
281
|
-
if (step.type === "count")
|
|
282
|
-
return { kind: "count" };
|
|
283
|
-
if (step.type === "aggregate")
|
|
284
|
-
return { kind: "aggregate" };
|
|
285
|
-
return { kind: "opaque" };
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Collect the server-stamped field names from the parsed [type] config:
|
|
289
|
-
* timestamps (`create`/`update` field names) + auto-populated field names
|
|
290
|
-
* (the keys of the `autoPopulatedFields` map). These appear on the READ
|
|
291
|
-
* record interface as OPTIONAL props.
|
|
292
|
-
*/
|
|
293
|
-
function collectStampedFields(typeConfig) {
|
|
294
|
-
const names = [];
|
|
295
|
-
const ts = typeConfig?.timestamps;
|
|
296
|
-
if (ts && typeof ts === "object") {
|
|
297
|
-
if (typeof ts.create === "string")
|
|
298
|
-
names.push(ts.create);
|
|
299
|
-
if (typeof ts.update === "string")
|
|
300
|
-
names.push(ts.update);
|
|
301
|
-
}
|
|
302
|
-
const apf = typeConfig?.autoPopulatedFields;
|
|
303
|
-
if (apf && typeof apf === "object") {
|
|
304
|
-
for (const key of Object.keys(apf))
|
|
305
|
-
names.push(key);
|
|
306
|
-
}
|
|
307
|
-
// De-dup, preserve first-seen order.
|
|
308
|
-
return [...new Set(names)];
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Convert the normalized op `params` map
|
|
312
|
-
* (`{ name: { type, required, enum?, items? } }`) into a sorted list of
|
|
313
|
-
* `ParamField`. Sorted by name for deterministic output. A string param's
|
|
314
|
-
* `enum` (#861) is carried through so the emitter can render a string-literal
|
|
315
|
-
* union.
|
|
316
|
-
*
|
|
317
|
-
* `inferredArrayElements` (#1488, Phase 1) maps a param bound to a `$in`/`$nin`
|
|
318
|
-
* filter operator to its inferred scalar element TS type (or the `"unknown"`
|
|
319
|
-
* sentinel → `unknown[]`). Inference only ever NARROWS a param the emitter
|
|
320
|
-
* would otherwise widen: an explicit `type = "array"` declaration (Phase 2)
|
|
321
|
-
* always wins, and a param absent from the map keeps its declared-type mapping.
|
|
322
|
-
*/
|
|
323
|
-
function paramsToFields(params, inferredArrayElements) {
|
|
324
|
-
if (!params || typeof params !== "object")
|
|
325
|
-
return [];
|
|
326
|
-
return Object.entries(params)
|
|
327
|
-
.map(([name, def]) => {
|
|
328
|
-
const rawType = def?.type;
|
|
329
|
-
const declaredType = rawType ?? "object";
|
|
330
|
-
const declaredItems = def?.items;
|
|
331
|
-
let type = declaredType;
|
|
332
|
-
let items = declaredItems;
|
|
333
|
-
// Apply inference only to untyped/`object` params — the declaration is
|
|
334
|
-
// otherwise authoritative (behavior 8). A param explicitly declared as a
|
|
335
|
-
// scalar (`string`/`number`/`boolean`/`integer`) is validated by runtime
|
|
336
|
-
// `execute` against that scalar type; a param declared with a model type
|
|
337
|
-
// is treated as model-typed by the runtime `coerceParam`, which rejects
|
|
338
|
-
// arrays with "must be an object"; and an explicit `array` already has its
|
|
339
|
-
// element type. Rewriting any of those to an inferred array would emit a
|
|
340
|
-
// type the server is guaranteed to reject. Only untyped/`object` params
|
|
341
|
-
// (which the emitter would otherwise widen to `Record<string, unknown>`)
|
|
342
|
-
// are eligible. Since `declaredType = rawType ?? "object"`, `=== "object"`
|
|
343
|
-
// matches both a missing `type` and an explicit `object` declaration.
|
|
344
|
-
const inferred = inferredArrayElements?.get(name);
|
|
345
|
-
if (declaredType === "object" && inferred !== undefined) {
|
|
346
|
-
type = "array";
|
|
347
|
-
// The `"unknown"` sentinel means "array, element unresolved": leave
|
|
348
|
-
// `items` undefined so the emitter renders `unknown[]`.
|
|
349
|
-
items = inferred === "unknown" ? undefined : inferred;
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
name,
|
|
353
|
-
type,
|
|
354
|
-
required: def?.required === true,
|
|
355
|
-
// Allowed-value set for a string param → string-literal union (#861).
|
|
356
|
-
// `normalizeOperationFromToml` preserves `enum` upstream via its
|
|
357
|
-
// `...rest` spread; carry it through here so it reaches the emitter.
|
|
358
|
-
enum: def?.enum,
|
|
359
|
-
items,
|
|
360
|
-
};
|
|
361
|
-
})
|
|
362
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Infer the scalar element TS type for op params consumed by a `$in`/`$nin`
|
|
366
|
-
* filter operator (#1488, Phase 1). Codegen-only: a param compared with
|
|
367
|
-
* `$in`/`$nin` against a model field is definitionally an array, so the
|
|
368
|
-
* typed-ops factory (#1441) should emit `<elem>[]` rather than widening an
|
|
369
|
-
* untyped/object param to `Record<string, unknown>` (which `string[]` is not
|
|
370
|
-
* assignable to).
|
|
371
|
-
*
|
|
372
|
-
* Kept a small local helper next to {@link inferRequiredRecordFields} rather
|
|
373
|
-
* than a shared cross-runtime analysis pass (sponsor decision on #1488).
|
|
374
|
-
*
|
|
375
|
-
* Returns a map of param name → element TS type. The `"unknown"` sentinel means
|
|
376
|
-
* "emit `unknown[]`" — used when the compared field is missing from the model
|
|
377
|
-
* schema (or the op has no resolvable model), when the same param is bound to
|
|
378
|
-
* fields of differing element types, or when the param is also used as a scalar
|
|
379
|
-
* elsewhere (conservative widen, mirroring #845's disagreement rule / #1488
|
|
380
|
-
* Fork 4). A param absent from the map is not consumed by any array operator
|
|
381
|
-
* and keeps its declared-type mapping (inference never widens such a param).
|
|
382
|
-
*/
|
|
383
|
-
export function inferArrayParamElementTypes(op, fieldTypesByModel) {
|
|
384
|
-
const result = new Map();
|
|
385
|
-
if (!op || typeof op !== "object")
|
|
386
|
-
return result;
|
|
387
|
-
const def = op.definition;
|
|
388
|
-
if (!def || typeof def !== "object")
|
|
389
|
-
return result;
|
|
390
|
-
// Element TS types seen for each param via `$in`/`$nin` (deduped). A param
|
|
391
|
-
// also seen as a scalar reference is widened to `unknown[]`.
|
|
392
|
-
const arrayElems = new Map();
|
|
393
|
-
const scalarRefs = new Set();
|
|
394
|
-
const addArrayElem = (param, elem) => {
|
|
395
|
-
let s = arrayElems.get(param);
|
|
396
|
-
if (!s) {
|
|
397
|
-
s = new Set();
|
|
398
|
-
arrayElems.set(param, s);
|
|
399
|
-
}
|
|
400
|
-
s.add(elem);
|
|
401
|
-
};
|
|
402
|
-
// A bare `$params.<name>` string → the param name, else null.
|
|
403
|
-
const paramRef = (v) => {
|
|
404
|
-
if (typeof v !== "string")
|
|
405
|
-
return null;
|
|
406
|
-
const m = v.match(/^\$params\.([A-Za-z0-9_]+)$/);
|
|
407
|
-
return m ? m[1] : null;
|
|
408
|
-
};
|
|
409
|
-
// Scalar element TS type for `field` of the current model, or `"unknown"`.
|
|
410
|
-
// A `stringset` field's ELEMENT is a string (#1488, e7) — do NOT reuse
|
|
411
|
-
// `tsTypeForDbFieldType`, which maps `stringset → string[]` and would emit
|
|
412
|
-
// `string[][]` for the param.
|
|
413
|
-
const elemTypeForField = (field, fields) => {
|
|
414
|
-
const ftype = fields?.get(field);
|
|
415
|
-
if (ftype === undefined)
|
|
416
|
-
return "unknown"; // missing field / no schema (e2)
|
|
417
|
-
if (ftype === "stringset")
|
|
418
|
-
return "string";
|
|
419
|
-
const ts = tsTypeForDbFieldType(ftype);
|
|
420
|
-
return ts === "string" || ts === "number" || ts === "boolean"
|
|
421
|
-
? ts
|
|
422
|
-
: "unknown";
|
|
423
|
-
};
|
|
424
|
-
const modelFields = (name) => typeof name === "string" ? fieldTypesByModel.get(name) : undefined;
|
|
425
|
-
// Walk a filter object, resolving field conditions against `fields`.
|
|
426
|
-
const walkFilter = (filter, fields) => {
|
|
427
|
-
if (!filter || typeof filter !== "object" || Array.isArray(filter))
|
|
428
|
-
return;
|
|
429
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
430
|
-
if (key === "$and" || key === "$or") {
|
|
431
|
-
if (Array.isArray(value)) {
|
|
432
|
-
for (const sub of value)
|
|
433
|
-
walkFilter(sub, fields);
|
|
434
|
-
}
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
// `$not` is a logical operator too, but (unlike `$and`/`$or`) it wraps a
|
|
438
|
-
// SINGLE nested condition object rather than an array — recurse into it so
|
|
439
|
-
// an `$in`/`$nin` inside a negated filter still infers its element type.
|
|
440
|
-
// Mirrors the operation grammar walker in
|
|
441
|
-
// `src/app-api/services/walk-operation.ts` (`walkFilterFields`), which
|
|
442
|
-
// treats `$and`/`$or`/`$not` as the recursing logical operators; the two
|
|
443
|
-
// walk the same op-definition filter grammar and must agree.
|
|
444
|
-
if (key === "$not") {
|
|
445
|
-
walkFilter(value, fields);
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
// Field condition: `key` is a field name; `value` is a scalar (shorthand
|
|
449
|
-
// equality) or an operator map (`{ $in: ..., $eq: ... }`).
|
|
450
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
451
|
-
for (const [operator, opVal] of Object.entries(value)) {
|
|
452
|
-
const p = paramRef(opVal);
|
|
453
|
-
if (!p)
|
|
454
|
-
continue;
|
|
455
|
-
if (operator === "$in" || operator === "$nin") {
|
|
456
|
-
addArrayElem(p, elemTypeForField(key, fields));
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
// A scalar operator ($eq/$ne/$gt/…) binds a scalar param.
|
|
460
|
-
scalarRefs.add(p);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
const p = paramRef(value);
|
|
466
|
-
if (p)
|
|
467
|
-
scalarRefs.add(p);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
// Top-level query filter.
|
|
472
|
-
walkFilter(def.filter, modelFields(op.modelName));
|
|
473
|
-
// `applyToQuery` ops hold their matching query under `definition.source`
|
|
474
|
-
// (`source.filter`), which resolves against the op's own model unless the
|
|
475
|
-
// source names a different one. Without this walk, an `$in`/`$nin` against an
|
|
476
|
-
// object/untyped param inside `source.filter` stays widened to
|
|
477
|
-
// `Record<string, unknown>` — the original array-param bug (#1488).
|
|
478
|
-
if (def.source && typeof def.source === "object") {
|
|
479
|
-
walkFilter(def.source.filter, modelFields(def.source.modelName ?? op.modelName));
|
|
480
|
-
}
|
|
481
|
-
// Mutation inner operations — each may carry its own filter + modelName.
|
|
482
|
-
if (Array.isArray(def.operations)) {
|
|
483
|
-
for (const inner of def.operations) {
|
|
484
|
-
if (inner && typeof inner === "object") {
|
|
485
|
-
walkFilter(inner.filter, modelFields(inner.modelName ?? op.modelName));
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
// Pipeline steps — each may carry its own filter + modelName.
|
|
490
|
-
if (Array.isArray(def.steps)) {
|
|
491
|
-
for (const step of def.steps) {
|
|
492
|
-
if (step && typeof step === "object") {
|
|
493
|
-
walkFilter(step.filter, modelFields(step.modelName ?? op.modelName));
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
for (const [param, elems] of arrayElems) {
|
|
498
|
-
if (scalarRefs.has(param) || elems.size !== 1) {
|
|
499
|
-
// Used as both array and scalar, or bound to differing element types.
|
|
500
|
-
result.set(param, "unknown");
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
result.set(param, [...elems][0]);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return result;
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Infer which record fields are always supplied at create time, by reading the
|
|
510
|
-
* `save` operation params (issue #842).
|
|
511
|
-
*
|
|
512
|
-
* A field is promoted to required for a model only if:
|
|
513
|
-
* - at least one `save` (create) op writes it from a bare `$params.X`
|
|
514
|
-
* binding, AND
|
|
515
|
-
* - EVERY `save` op that writes the field binds it to a param declared
|
|
516
|
-
* `required: true` (the "ALL writers agree" rule).
|
|
517
|
-
*
|
|
518
|
-
* Deliberately conservative:
|
|
519
|
-
* - `patch` (and every non-`save` inner op) is excluded — partial updates
|
|
520
|
-
* don't guarantee creation-time presence.
|
|
521
|
-
* - whole-object saves (`data = "$params.X"`, a string rather than a
|
|
522
|
-
* per-field map) carry no field-level mapping, so no field can be promoted
|
|
523
|
-
* from them (Q4 skip).
|
|
524
|
-
* - only an EXACT top-level bare `$params.X` value counts (the whole binding
|
|
525
|
-
* value must match `^\$params\.X$`, and `X` must be a declared top-level
|
|
526
|
-
* param). A literal, computed, partially-interpolated value, or a nested
|
|
527
|
-
* path (`$params.obj.sub`) never promotes a field — a nested sub-property
|
|
528
|
-
* may be absent even when the top-level object param is required. This is
|
|
529
|
-
* deliberately stricter than the CLI's lenient `collectParamRefs`
|
|
530
|
-
* first-segment rule, which is fine for its validation use but would
|
|
531
|
-
* wrongly promote nested bindings here.
|
|
532
|
-
* - inner ops are matched to the record model by the inner op's own
|
|
533
|
-
* `modelName` when present, falling back to the enclosing operation's
|
|
534
|
-
* `modelName`.
|
|
535
|
-
*
|
|
536
|
-
* Pure: no I/O, no mutation of the input. The caller OR's the result with the
|
|
537
|
-
* schema-level `required` flag, so inference only ever ADDS requiredness.
|
|
538
|
-
*/
|
|
539
|
-
export function inferRequiredRecordFields(operations) {
|
|
540
|
-
// model → field → running verdict. `true` = every save op seen so far that
|
|
541
|
-
// sets the field bound it to a required param; `false` = at least one save op
|
|
542
|
-
// set it from an optional/absent param (sticky — never recovers).
|
|
543
|
-
const verdicts = new Map();
|
|
544
|
-
for (const op of operations ?? []) {
|
|
545
|
-
if (!op || op.type !== "mutation")
|
|
546
|
-
continue;
|
|
547
|
-
const innerOps = op.definition?.operations;
|
|
548
|
-
if (!Array.isArray(innerOps))
|
|
549
|
-
continue;
|
|
550
|
-
const params = op.params && typeof op.params === "object" ? op.params : {};
|
|
551
|
-
for (const inner of innerOps) {
|
|
552
|
-
if (!inner || inner.op !== "save")
|
|
553
|
-
continue;
|
|
554
|
-
// Whole-object save (`data` is a string $params ref) has no per-field
|
|
555
|
-
// map — skip (Q4).
|
|
556
|
-
const data = inner.data;
|
|
557
|
-
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
558
|
-
continue;
|
|
559
|
-
const modelName = inner.modelName ?? op.modelName;
|
|
560
|
-
if (typeof modelName !== "string" || modelName === "")
|
|
561
|
-
continue;
|
|
562
|
-
let fieldVerdicts = verdicts.get(modelName);
|
|
563
|
-
if (!fieldVerdicts) {
|
|
564
|
-
fieldVerdicts = new Map();
|
|
565
|
-
verdicts.set(modelName, fieldVerdicts);
|
|
566
|
-
}
|
|
567
|
-
for (const [field, value] of Object.entries(data)) {
|
|
568
|
-
// Only an EXACT top-level bare binding (`$params.X`, no further `.sub`
|
|
569
|
-
// path) counts. `collectParamRefs` is intentionally lenient — it
|
|
570
|
-
// returns the FIRST dotted segment, so `$params.profile.name` yields
|
|
571
|
-
// `["profile"]` and would wrongly promote a field that actually reads
|
|
572
|
-
// a nested sub-property that may be absent. We match the whole value
|
|
573
|
-
// against `^\$params\.X$` here instead, leaving `collectParamRefs`
|
|
574
|
-
// untouched for its validation use. Partial interpolation
|
|
575
|
-
// (`prefix-$params.X`), literals, and nested paths all fail this match
|
|
576
|
-
// and therefore never promote a field.
|
|
577
|
-
const match = typeof value === "string"
|
|
578
|
-
? value.match(/^\$params\.([A-Za-z0-9_]+)$/)
|
|
579
|
-
: null;
|
|
580
|
-
if (!match) {
|
|
581
|
-
// Not an exact top-level bare param binding — this save op cannot
|
|
582
|
-
// prove the field required. Mark it not-required for this writer.
|
|
583
|
-
fieldVerdicts.set(field, false);
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
const paramName = match[1];
|
|
587
|
-
// The matched name must resolve to a declared top-level param.
|
|
588
|
-
const required = params[paramName]?.required === true;
|
|
589
|
-
const prior = fieldVerdicts.get(field);
|
|
590
|
-
// ALL-writers semantics: stays `true` only while every save writer
|
|
591
|
-
// agrees; one disagreeing writer flips it to `false` permanently.
|
|
592
|
-
fieldVerdicts.set(field, prior === false ? false : required);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
const result = new Map();
|
|
597
|
-
for (const [modelName, fieldVerdicts] of verdicts) {
|
|
598
|
-
const fields = new Set();
|
|
599
|
-
for (const [field, verdict] of fieldVerdicts) {
|
|
600
|
-
if (verdict)
|
|
601
|
-
fields.add(field);
|
|
602
|
-
}
|
|
603
|
-
if (fields.size > 0)
|
|
604
|
-
result.set(modelName, { fields });
|
|
605
|
-
}
|
|
606
|
-
return result;
|
|
607
|
-
}
|
|
608
145
|
/**
|
|
609
146
|
* Run codegen end-to-end across one or more database-type TOML inputs. The
|
|
610
147
|
* disk lifecycle (write / `--check` / stale cleanup) is delegated to the
|