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.
Files changed (68) hide show
  1. package/dist/bin/primitive.js +2 -0
  2. package/dist/bin/primitive.js.map +1 -1
  3. package/dist/src/commands/apps.js +54 -2
  4. package/dist/src/commands/apps.js.map +1 -1
  5. package/dist/src/commands/databases.js +31 -10
  6. package/dist/src/commands/databases.js.map +1 -1
  7. package/dist/src/commands/documents.js +77 -0
  8. package/dist/src/commands/documents.js.map +1 -1
  9. package/dist/src/commands/settings.d.ts +15 -0
  10. package/dist/src/commands/settings.js +102 -0
  11. package/dist/src/commands/settings.js.map +1 -0
  12. package/dist/src/commands/sync-app-settings.d.ts +105 -0
  13. package/dist/src/commands/sync-app-settings.js +339 -0
  14. package/dist/src/commands/sync-app-settings.js.map +1 -0
  15. package/dist/src/commands/sync.d.ts +34 -19
  16. package/dist/src/commands/sync.js +373 -200
  17. package/dist/src/commands/sync.js.map +1 -1
  18. package/dist/src/commands/workflows.js +32 -10
  19. package/dist/src/commands/workflows.js.map +1 -1
  20. package/dist/src/lib/api-client.d.ts +2 -0
  21. package/dist/src/lib/api-client.js +6 -0
  22. package/dist/src/lib/api-client.js.map +1 -1
  23. package/dist/src/lib/app-settings-descriptor.d.ts +108 -0
  24. package/dist/src/lib/app-settings-descriptor.js +250 -0
  25. package/dist/src/lib/app-settings-descriptor.js.map +1 -0
  26. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +15 -0
  27. package/dist/src/lib/codegen-shared/generatedFiles.js +27 -10
  28. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  29. package/dist/src/lib/db-codegen/dbGenerator.d.ts +11 -83
  30. package/dist/src/lib/db-codegen/dbGenerator.js +93 -556
  31. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
  32. package/dist/src/lib/db-codegen/dbNaming.d.ts +15 -7
  33. package/dist/src/lib/db-codegen/dbNaming.js +25 -13
  34. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
  35. package/dist/src/lib/db-codegen/dbTemplates.d.ts +55 -4
  36. package/dist/src/lib/db-codegen/dbTemplates.js +169 -40
  37. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  38. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
  39. package/dist/src/lib/db-codegen/dbTypeIR.js +517 -0
  40. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
  41. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +101 -0
  42. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +200 -0
  43. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
  44. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +68 -0
  45. package/dist/src/lib/swift-codegen/dbGenerator.js +380 -0
  46. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
  47. package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
  48. package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
  49. package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
  50. package/dist/src/lib/swift-codegen/generator.d.ts +84 -0
  51. package/dist/src/lib/swift-codegen/generator.js +178 -0
  52. package/dist/src/lib/swift-codegen/generator.js.map +1 -0
  53. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +72 -0
  54. package/dist/src/lib/swift-codegen/schemaToSwift.js +644 -0
  55. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
  56. package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
  57. package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
  58. package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
  59. package/dist/src/lib/sync-resource-types.d.ts +225 -0
  60. package/dist/src/lib/sync-resource-types.js +394 -0
  61. package/dist/src/lib/sync-resource-types.js.map +1 -0
  62. package/dist/src/lib/workflow-codegen/generator.d.ts +18 -5
  63. package/dist/src/lib/workflow-codegen/generator.js +116 -41
  64. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  65. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +85 -0
  66. package/dist/src/lib/workflow-codegen/invokerIR.js +71 -0
  67. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
  68. package/package.json +4 -2
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Shared layout metadata for the per-entity resource types `sync pull` writes
3
+ * into a sync slot (issue #1659).
4
+ *
5
+ * `sync pull` writes one file per server entity and records what it wrote in
6
+ * `.primitive-sync.json` (`SyncState.entities[<slot>]`). Until #1659 it never
7
+ * removed a local file whose entity had disappeared server-side, so the next
8
+ * `sync push` — which treats local TOML as the source of truth — silently
9
+ * re-created the just-deleted entity.
10
+ *
11
+ * This module holds the narrow descriptor the prune pass and `sync diff` share:
12
+ * directory, extension, state slot, key→filename derivation, and the sidecar
13
+ * test-directory block type. The pull write loops deliberately stay as they are
14
+ * — this table is layout metadata, not a write framework.
15
+ */
16
+ import { existsSync, rmSync, statSync, unlinkSync } from "fs";
17
+ import { dirname, join } from "path";
18
+ const tomlFile = (key) => `${key}.toml`;
19
+ /**
20
+ * Rule sets are keyed on disk (and in sync state) by their name with
21
+ * filesystem-hostile characters replaced. Shared by the pull write loop and the
22
+ * prune pass so the two can never derive a different key for the same rule set.
23
+ */
24
+ export function ruleSetFileKey(ruleSet) {
25
+ return String(ruleSet.name || ruleSet.ruleSetId || "").replace(/[/\\:*?"<>|]/g, "_");
26
+ }
27
+ /**
28
+ * Every per-entity resource type in a sync slot. `app.toml` and `vars.toml` are
29
+ * root singletons, not per-entity directories, so they are not listed here —
30
+ * var removal is already handled within-file by `sync push` (`planVarsPush`).
31
+ */
32
+ export const SYNC_RESOURCE_TYPES = [
33
+ {
34
+ label: "integration",
35
+ dir: "integrations",
36
+ ext: ".toml",
37
+ stateSlot: "integrations",
38
+ fileFromKey: tomlFile,
39
+ // `/admin/api/apps/{id}/integrations` paginates; pull drains it with fetchAll.
40
+ listingComplete: true,
41
+ },
42
+ {
43
+ label: "webhook",
44
+ dir: "webhooks",
45
+ ext: ".toml",
46
+ stateSlot: "webhooks",
47
+ fileFromKey: tomlFile,
48
+ // `/admin/api/apps/{id}/webhooks` paginates; pull drains it with fetchAll.
49
+ listingComplete: true,
50
+ },
51
+ {
52
+ label: "cron-trigger",
53
+ dir: "cron-triggers",
54
+ ext: ".toml",
55
+ stateSlot: "cronTriggers",
56
+ fileFromKey: tomlFile,
57
+ // `{ limit: 100 }`, no cursor, archived filtered after the query.
58
+ listingComplete: false,
59
+ },
60
+ {
61
+ label: "blob-bucket",
62
+ dir: "blob-buckets",
63
+ ext: ".toml",
64
+ stateSlot: "blobBuckets",
65
+ fileFromKey: tomlFile,
66
+ // `queryByIndex("bucketsForApp")` — no cursor, one page.
67
+ listingComplete: false,
68
+ },
69
+ {
70
+ label: "prompt",
71
+ dir: "prompts",
72
+ ext: ".toml",
73
+ stateSlot: "prompts",
74
+ fileFromKey: tomlFile,
75
+ testBlockType: "prompt",
76
+ // `/admin/api/apps/{id}/prompts` paginates; pull drains it with fetchAll.
77
+ listingComplete: true,
78
+ },
79
+ {
80
+ label: "workflow",
81
+ dir: "workflows",
82
+ ext: ".toml",
83
+ stateSlot: "workflows",
84
+ fileFromKey: tomlFile,
85
+ testBlockType: "workflow",
86
+ // `/admin/api/apps/{id}/workflows` paginates; pull drains it with fetchAll.
87
+ listingComplete: true,
88
+ },
89
+ {
90
+ // Rhai scripts: `.rhai` bodies, not TOML, with tests at
91
+ // `transforms/<name>.tests/`.
92
+ label: "transform",
93
+ dir: "transforms",
94
+ ext: ".rhai",
95
+ stateSlot: "scripts",
96
+ fileFromKey: (key) => `${key}.rhai`,
97
+ testBlockType: "script",
98
+ // `queryByIndex("scriptsByApp")` — no cursor, one page.
99
+ listingComplete: false,
100
+ },
101
+ {
102
+ label: "database-type",
103
+ dir: "database-types",
104
+ ext: ".toml",
105
+ stateSlot: "databaseTypes",
106
+ fileFromKey: tomlFile,
107
+ // `queryByIndex("databaseTypeConfigsForApp")` — no cursor, one page.
108
+ listingComplete: false,
109
+ },
110
+ {
111
+ // The state key is already the sanitized filename the write loop derives
112
+ // from the rule set's name (`name.replace(/[/\\:*?"<>|]/g, "_")`), so the
113
+ // filename is the key plus the extension.
114
+ label: "rule-set",
115
+ dir: "rule-sets",
116
+ ext: ".toml",
117
+ stateSlot: "ruleSets",
118
+ fileFromKey: tomlFile,
119
+ // `queryByIndex("ruleSetsForApp")` — no cursor, one page.
120
+ listingComplete: false,
121
+ },
122
+ {
123
+ label: "group-type-config",
124
+ dir: "group-type-configs",
125
+ ext: ".toml",
126
+ stateSlot: "groupTypeConfigs",
127
+ fileFromKey: tomlFile,
128
+ // `queryByIndex("configsForApp")` — no cursor, one page.
129
+ listingComplete: false,
130
+ },
131
+ {
132
+ label: "collection-type-config",
133
+ dir: "collection-type-configs",
134
+ ext: ".toml",
135
+ stateSlot: "collectionTypeConfigs",
136
+ fileFromKey: tomlFile,
137
+ // `queryByIndex("collectionConfigsForApp")` — no cursor, one page.
138
+ listingComplete: false,
139
+ },
140
+ {
141
+ // State key is the `resourceType#category` pair; the file on disk is
142
+ // `resourceType.category.toml`.
143
+ label: "metadata-category-config",
144
+ dir: "metadata-category-configs",
145
+ ext: ".toml",
146
+ stateSlot: "metadataCategoryConfigs",
147
+ fileFromKey: (key) => `${key.replace("#", ".")}.toml`,
148
+ // `queryByIndex("metadataCategoriesForApp")` — no cursor, one page.
149
+ listingComplete: false,
150
+ },
151
+ {
152
+ label: "email-template",
153
+ dir: "email-templates",
154
+ ext: ".toml",
155
+ stateSlot: "emailTemplates",
156
+ fileFromKey: tomlFile,
157
+ // `queryByIndex("templatesByApp")` — no cursor, one page.
158
+ listingComplete: false,
159
+ },
160
+ ];
161
+ /** Generic try/catch wrapper for a server fetch, in the ok/error shape. */
162
+ export async function attemptFetch(fn) {
163
+ try {
164
+ return { ok: true, value: await fn() };
165
+ }
166
+ catch (error) {
167
+ return { ok: false, error };
168
+ }
169
+ }
170
+ /**
171
+ * A filename must stay inside its type's directory. State is read from a file
172
+ * on disk, so a corrupt or hand-edited `.primitive-sync.json` must never be
173
+ * able to steer an `unlinkSync` outside the sync slot.
174
+ */
175
+ export function isSafeSyncFilename(filename) {
176
+ if (!filename || filename === "." || filename === "..")
177
+ return false;
178
+ if (filename.includes("/") || filename.includes("\\"))
179
+ return false;
180
+ if (filename.includes("\0"))
181
+ return false;
182
+ return true;
183
+ }
184
+ /**
185
+ * Decide which of a type's managed entity keys should have their local file
186
+ * removed, given the prior sync state and the outcome of this pull's fetch.
187
+ *
188
+ * A key is pruned iff it was **managed** (present in the prior
189
+ * `SyncState.entities[<slot>]`, so a previous pull wrote its file) and is
190
+ * **absent from the server keys** this pull fetched. A local file that was
191
+ * never in prior state was hand-authored — it is left alone and reported by
192
+ * `sync diff` as new.
193
+ *
194
+ * Pure and side-effect-free (the caller owns the `unlinkSync`) so the safety
195
+ * rules are unit-testable without a live server.
196
+ */
197
+ export function planPrune(input) {
198
+ const { resourceType, priorKeys, presence, enabled } = input;
199
+ // Which managed keys the server did not list. Only meaningful when the fetch
200
+ // succeeded; on a failed fetch we know nothing and must not act.
201
+ const absentKeys = presence.ok
202
+ ? priorKeys.filter((key) => !new Set(presence.serverKeys).has(key))
203
+ : [];
204
+ if (!enabled) {
205
+ return {
206
+ keys: [],
207
+ skipped: true,
208
+ skipReason: "disabled",
209
+ unsafeKeys: [],
210
+ unprunedAbsentKeys: absentKeys,
211
+ };
212
+ }
213
+ if (!presence.ok) {
214
+ return {
215
+ keys: [],
216
+ skipped: true,
217
+ skipReason: "fetch-failed",
218
+ unsafeKeys: [],
219
+ unprunedAbsentKeys: [],
220
+ };
221
+ }
222
+ // A 200 is not a complete list. Deleting a file because a truncated listing
223
+ // omitted its entity is unrecoverable-looking to the operator (the entity is
224
+ // still live, so no later pull rewrites the file), which is worse than
225
+ // leaving a stale file behind. See `listingComplete`.
226
+ if (!resourceType.listingComplete) {
227
+ return {
228
+ keys: [],
229
+ skipped: true,
230
+ skipReason: "listing-incomplete",
231
+ unsafeKeys: [],
232
+ unprunedAbsentKeys: absentKeys,
233
+ };
234
+ }
235
+ const keys = [];
236
+ const unsafeKeys = [];
237
+ for (const key of absentKeys) {
238
+ if (!isSafeSyncFilename(resourceType.fileFromKey(key))) {
239
+ unsafeKeys.push(key);
240
+ continue;
241
+ }
242
+ keys.push(key);
243
+ }
244
+ return { keys, skipped: false, unsafeKeys, unprunedAbsentKeys: [] };
245
+ }
246
+ /**
247
+ * Copy prior state entries for entities the server listed but this pull did not
248
+ * write (e.g. a script with no active config, or an email template whose detail
249
+ * fetch failed). Without this the entity would drop out of the state slot and
250
+ * look hand-authored to the next pull, so its stale file could never be pruned.
251
+ */
252
+ export function preserveUnwrittenEntries(written, prior, serverKeys) {
253
+ if (!prior)
254
+ return written;
255
+ for (const key of serverKeys) {
256
+ if (!(key in written) && key in prior) {
257
+ written[key] = prior[key];
258
+ }
259
+ }
260
+ return written;
261
+ }
262
+ /**
263
+ * Best-effort check for uncommitted git changes at `path` (a file or a
264
+ * directory). Returns false when git isn't available, the path isn't in a
265
+ * repository, or anything else goes wrong — the caller treats "unknown" as
266
+ * "no local edits", so this never blocks on a non-git checkout.
267
+ *
268
+ * `path` is passed as an argument, never interpolated into a shell string:
269
+ * on the prune path its basename derives from a server entity name, and entity
270
+ * names are free-form (a rule set may legitimately be named ``a`b`` or `a$b`).
271
+ * `execFileSync` runs git directly with no shell, so no name can be read as
272
+ * a command.
273
+ *
274
+ * `includeUntracked` decides whether an untracked file counts as a change:
275
+ * - `true` (default) suits "am I about to overwrite something?" — `sync revert`
276
+ * would clobber an untracked file just the same.
277
+ * - `false` suits "did the operator edit this file?" — an untracked file is the
278
+ * ordinary state of a pulled file in a sync dir that was never committed, and
279
+ * is not evidence of a local edit.
280
+ */
281
+ export async function hasUncommittedChanges(path, options = {}) {
282
+ const { includeUntracked = true } = options;
283
+ if (!existsSync(path))
284
+ return false;
285
+ try {
286
+ const { execFileSync } = await import("child_process");
287
+ // Run git from the path's own location, not the process cwd: the sync slot
288
+ // can sit in a different repository than the one the CLI was invoked from
289
+ // (or in none at all).
290
+ const cwd = statSync(path).isDirectory() ? path : dirname(path);
291
+ const args = ["status", "--porcelain"];
292
+ if (!includeUntracked)
293
+ args.push("-uno");
294
+ args.push("--", path);
295
+ const out = execFileSync("git", args, {
296
+ cwd,
297
+ stdio: ["ignore", "pipe", "ignore"],
298
+ encoding: "utf-8",
299
+ });
300
+ return out.trim().length > 0;
301
+ }
302
+ catch {
303
+ return false;
304
+ }
305
+ }
306
+ /**
307
+ * The phrase pull and diff both use for an entity that is no longer exported.
308
+ *
309
+ * The plan of record asked for "archived or deleted" on the assumption that
310
+ * archiving drops an entity from the export. It does not: the list endpoints
311
+ * filter by status only when `?status=` is passed and pull never passes it
312
+ * (`listAppPrompts`/`listAppIntegrations` in `src/admin-api.ts`; `fetchAll` in
313
+ * `cli/src/commands/sync.ts`). An archived entity stays in the export and its
314
+ * TOML is rewritten with `status = "archived"`, so it is never pruned. The only
315
+ * way to be absent from the export is to have been deleted.
316
+ */
317
+ export const ABSENT_FROM_EXPORT = "absent from sync export (deleted on server)";
318
+ /** `sync diff` status for a local file whose managed entity is gone. */
319
+ export const LOCAL_ONLY_ABSENT = "local only (absent from export)";
320
+ /** `sync diff` status for a local file the operator authored themselves. */
321
+ export const LOCAL_ONLY_NEW = "local only";
322
+ /**
323
+ * Classify a local file that has no matching server entity (issue #1659).
324
+ *
325
+ * The two cases look identical on disk but mean opposite things. A key a prior
326
+ * pull managed means the entity was deleted server-side and this stale file
327
+ * will RE-create it on the next push. A key never pulled means the operator
328
+ * authored the file and push creating it is the intent.
329
+ */
330
+ export function classifyLocalOnly(priorEntities, slot, key) {
331
+ const managed = (priorEntities?.[slot] ?? {});
332
+ return key in managed ? LOCAL_ONLY_ABSENT : LOCAL_ONLY_NEW;
333
+ }
334
+ /**
335
+ * Remove the local files for `keys` (as decided by `planPrune`), along with any
336
+ * sidecar `<key>.tests/` directory.
337
+ *
338
+ * A file with uncommitted git edits is kept and reported instead: the operator
339
+ * may be mid-way through authoring a change they meant to push. Everything
340
+ * removed here is recoverable from the pre-pull snapshot via `sync revert`.
341
+ */
342
+ export async function applyPrune(options) {
343
+ const { configDir, resourceType, keys, testCaseEntities, logger } = options;
344
+ const log = logger ?? (() => { });
345
+ const result = {
346
+ prunedKeys: [],
347
+ removed: [],
348
+ keptWithLocalEdits: [],
349
+ removedTestCaseKeys: [],
350
+ };
351
+ for (const key of keys) {
352
+ const filename = resourceType.fileFromKey(key);
353
+ const relativePath = `${resourceType.dir}/${filename}`;
354
+ const filePath = join(configDir, resourceType.dir, filename);
355
+ if (!existsSync(filePath))
356
+ continue;
357
+ // Untracked does not mean edited: in a sync dir that was never committed
358
+ // (or in no repository at all) every pulled file is untracked, and counting
359
+ // that as a local edit would keep every stale file and leave #1659 unfixed.
360
+ if (await hasUncommittedChanges(filePath, { includeUntracked: false })) {
361
+ result.keptWithLocalEdits.push(key);
362
+ log(` Kept ${relativePath} — ${ABSENT_FROM_EXPORT} but the file has ` +
363
+ "uncommitted local edits; remove it yourself or re-create the entity on the server");
364
+ continue;
365
+ }
366
+ unlinkSync(filePath);
367
+ result.prunedKeys.push(key);
368
+ result.removed.push(relativePath);
369
+ log(` Removed ${relativePath} — ${ABSENT_FROM_EXPORT}`);
370
+ if (!resourceType.testBlockType)
371
+ continue;
372
+ // Sidecar tests: `<key>.tests/` holds the block's test-case TOMLs and their
373
+ // attachment directories. With the block gone there is nothing to run them
374
+ // against, so they go too — along with their sync-state records.
375
+ const testsDirName = `${key}.tests`;
376
+ const testsDir = join(configDir, resourceType.dir, testsDirName);
377
+ if (existsSync(testsDir) && isSafeSyncFilename(testsDirName)) {
378
+ rmSync(testsDir, { recursive: true, force: true });
379
+ result.removed.push(`${resourceType.dir}/${testsDirName}`);
380
+ log(` Removed ${resourceType.dir}/${testsDirName} — ${ABSENT_FROM_EXPORT}`);
381
+ }
382
+ if (testCaseEntities) {
383
+ for (const [stateKey, entry] of Object.entries(testCaseEntities)) {
384
+ if (entry?.blockType === resourceType.testBlockType &&
385
+ entry?.blockKey === key) {
386
+ delete testCaseEntities[stateKey];
387
+ result.removedTestCaseKeys.push(stateKey);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ return result;
393
+ }
394
+ //# sourceMappingURL=sync-resource-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-resource-types.js","sourceRoot":"","sources":["../../../src/lib/sync-resource-types.ts"],"names":[],"mappings":"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"}
@@ -16,11 +16,24 @@
16
16
  * `schema-validation` descriptor) so the generated types match runtime
17
17
  * validation exactly. A malformed schema throws (fail-and-exit).
18
18
  * - `.runSync` is emitted only for `syncCallable: true` workflows (the server
19
- * rejects run-sync otherwise); `.start` and `.getStatus` for every
20
- * non-reserved workflow (the typed async path, issue #1512 — async-only
21
- * workflows are exactly the ones that need a typed `getStatus`). No
22
- * `.terminate` member is emitted (the factory stays a start/status helper;
23
- * typed termination is available via the client's explicit `terminate<O>`).
19
+ * rejects run-sync otherwise); `.start`, `.getStatus`, and `.terminate` for
20
+ * every non-reserved workflow (the typed async path, issue #1512 —
21
+ * async-only workflows are exactly the ones that need a typed `getStatus` /
22
+ * `terminate`). `.terminate` binds `<Key>Output` (a terminated run can carry
23
+ * partial output); issue #1543 added the member, reversing #1512's
24
+ * start/status-only decision.
25
+ * - `.define` is emitted only for an apply-mode workflow
26
+ * (`requiresClientApply !== false`, the server's default). It binds
27
+ * `<Key>Output` so the `onApply` handler's `output` is the workflow's real
28
+ * output type rather than `any` (issue #1543, on top of #1542's
29
+ * `define<O>` / `WorkflowDefineOptions<O>` generics). `getPendingApplies`
30
+ * gets no member — #1542 made it a concrete `PendingApply[]` with no output
31
+ * to bind, and it is scoped to a document rather than a workflow.
32
+ * - `.cronTriggers.{create,update}` bind `<Key>Input` to the trigger's
33
+ * `rootInput` and pin `workflowKey`, so a cron helper can't be redirected at
34
+ * another workflow (issue #1543). `rootInput` is `Partial<<Key>Input>` for
35
+ * an object-shaped input schema (an `inputMapping` may supply the rest) and
36
+ * the full `<Key>Input` otherwise.
24
37
  * - `input` is a REQUIRED option iff the schema rejects `{}` (the client's
25
38
  * `input ?? {}` fallback would fail validation); otherwise `input?`.
26
39
  * - The runtime call always passes the ORIGINAL key string; only identifiers
@@ -16,11 +16,24 @@
16
16
  * `schema-validation` descriptor) so the generated types match runtime
17
17
  * validation exactly. A malformed schema throws (fail-and-exit).
18
18
  * - `.runSync` is emitted only for `syncCallable: true` workflows (the server
19
- * rejects run-sync otherwise); `.start` and `.getStatus` for every
20
- * non-reserved workflow (the typed async path, issue #1512 — async-only
21
- * workflows are exactly the ones that need a typed `getStatus`). No
22
- * `.terminate` member is emitted (the factory stays a start/status helper;
23
- * typed termination is available via the client's explicit `terminate<O>`).
19
+ * rejects run-sync otherwise); `.start`, `.getStatus`, and `.terminate` for
20
+ * every non-reserved workflow (the typed async path, issue #1512 —
21
+ * async-only workflows are exactly the ones that need a typed `getStatus` /
22
+ * `terminate`). `.terminate` binds `<Key>Output` (a terminated run can carry
23
+ * partial output); issue #1543 added the member, reversing #1512's
24
+ * start/status-only decision.
25
+ * - `.define` is emitted only for an apply-mode workflow
26
+ * (`requiresClientApply !== false`, the server's default). It binds
27
+ * `<Key>Output` so the `onApply` handler's `output` is the workflow's real
28
+ * output type rather than `any` (issue #1543, on top of #1542's
29
+ * `define<O>` / `WorkflowDefineOptions<O>` generics). `getPendingApplies`
30
+ * gets no member — #1542 made it a concrete `PendingApply[]` with no output
31
+ * to bind, and it is scoped to a document rather than a workflow.
32
+ * - `.cronTriggers.{create,update}` bind `<Key>Input` to the trigger's
33
+ * `rootInput` and pin `workflowKey`, so a cron helper can't be redirected at
34
+ * another workflow (issue #1543). `rootInput` is `Partial<<Key>Input>` for
35
+ * an object-shaped input schema (an `inputMapping` may supply the rest) and
36
+ * the full `<Key>Input` otherwise.
24
37
  * - `input` is a REQUIRED option iff the schema rejects `{}` (the client's
25
38
  * `input ?? {}` fallback would fail validation); otherwise `input?`.
26
39
  * - The runtime call always passes the ORIGINAL key string; only identifiers
@@ -30,26 +43,16 @@
30
43
  * shared `writeOrCheckGeneratedFiles`, exactly like `databases codegen`.
31
44
  */
32
45
  import * as path from "path";
33
- import { parseConfigToml } from "../config-toml.js";
34
- import { safeJsonParse } from "../workflow-payload.js";
35
46
  import { writeOrCheckGeneratedFiles, } from "../codegen-shared/generatedFiles.js";
36
47
  import { fingerprintToml } from "../db-codegen/dbFingerprint.js";
37
- import { schemaToTsType, schemaRejectsEmptyObject } from "./schemaToTs.js";
48
+ import { schemaToTsType } from "./schemaToTs.js";
38
49
  import { describeSchema } from "./generated-schema-descriptor.js";
50
+ import { describeWorkflowInvoker } from "./invokerIR.js";
39
51
  import { workflowInputTypeName, workflowOutputTypeName, workflowFactoryName, } from "./naming.js";
40
- /** Prefix marking a platform-owned workflow key (mirrors the server's
41
- * `INTERNAL_WORKFLOW_KEY_PREFIX`; inlined because the CLI cannot import from
42
- * `src/`). Such workflows are never client-invocable and are skipped. */
43
- const INTERNAL_WORKFLOW_KEY_PREFIX = "__internal.";
44
52
  /** Suffix identifying a generated file (shared with `databases codegen`). */
45
53
  export const GENERATED_FILE_SUFFIX = ".generated.ts";
46
54
  /** The client package the generated file imports its types from. */
47
55
  const CLIENT_PACKAGE = "js-bao-wss-client";
48
- /** True for a platform-owned workflow key. */
49
- function isReservedKey(key) {
50
- return (typeof key === "string" &&
51
- key.trim().toLowerCase().startsWith(INTERNAL_WORKFLOW_KEY_PREFIX));
52
- }
53
56
  /**
54
57
  * Render a top-level `<Name>Input`/`<Name>Output` declaration. An absent schema
55
58
  * → `unknown`. A plain object type → an `interface`; anything else → a `type`
@@ -74,35 +77,39 @@ function renderTypeDecl(name, schema) {
74
77
  * Parse a single workflow TOML into the data the emitter needs. Throws on
75
78
  * malformed schema JSON (fail-and-exit) — the honest `unknown` fallback is for
76
79
  * valid-but-untypeable schemas only. Returns null for a reserved workflow.
80
+ *
81
+ * The operation metadata (key, `syncCallable`, required-vs-optional `input`,
82
+ * parsed schemas) comes from the SHARED `describeWorkflowInvoker` IR so the TS
83
+ * and Swift invokers can never disagree (#1547 binding item 5). Only the
84
+ * TS-specific naming + type-declaration rendering happens here.
77
85
  */
78
86
  function parseWorkflow(input) {
79
87
  // #1464 removed `@iarna/toml`; #1446 makes `sync pull` emit schemas as native
80
88
  // TOML tables (incl. mixed-type enums like `enum = ["ok", 1]`, TOML 1.0). The
81
- // `parseConfigToml` facade parses both. `safeJsonParse` still owns the
82
- // string-vs-inline-table schema handling.
83
- const data = parseConfigToml(input.tomlContent);
84
- const workflow = data?.workflow ?? {};
85
- const key = typeof workflow.key === "string" && workflow.key.length > 0
86
- ? workflow.key
87
- : input.fileStem;
88
- if (isReservedKey(key))
89
- return null;
90
- // safeJsonParse throws on invalid JSON in a schema string — let it bubble.
91
- const inputSchema = safeJsonParse(workflow.inputSchema);
92
- const outputSchema = safeJsonParse(workflow.outputSchema);
93
- const inputTypeName = workflowInputTypeName(key);
94
- const outputTypeName = workflowOutputTypeName(key);
89
+ // shared IR uses `parseConfigToml` + `safeJsonParse` under the hood.
90
+ const ir = describeWorkflowInvoker({
91
+ fileStem: input.fileStem,
92
+ tomlContent: input.tomlContent,
93
+ });
94
+ if (!ir)
95
+ return null; // reserved workflow
96
+ const inputTypeName = workflowInputTypeName(ir.key);
97
+ const outputTypeName = workflowOutputTypeName(ir.key);
95
98
  return {
96
- key,
97
- syncCallable: workflow.syncCallable === true,
99
+ key: ir.key,
100
+ syncCallable: ir.syncCallable,
98
101
  inputTypeName,
99
102
  outputTypeName,
100
- factoryName: workflowFactoryName(key),
101
- inputDecl: renderTypeDecl(inputTypeName, inputSchema),
102
- outputDecl: renderTypeDecl(outputTypeName, outputSchema),
103
- // `input` is required iff the schema rejects the client's `{}` fallback.
104
- // A schema-less workflow (`undefined`) accepts anything → optional.
105
- inputRequired: inputSchema !== undefined && schemaRejectsEmptyObject(inputSchema),
103
+ factoryName: workflowFactoryName(ir.key),
104
+ inputDecl: renderTypeDecl(inputTypeName, ir.inputSchema),
105
+ outputDecl: renderTypeDecl(outputTypeName, ir.outputSchema),
106
+ inputRequired: ir.inputRequired,
107
+ // Object-shaped only when the schema declares exactly `type: "object"`
108
+ // the one case where `Partial<<Key>Input>` is a sound cron `rootInput`
109
+ // type. Schema-less / union / scalar / array → not object-shaped.
110
+ inputIsObjectShaped: ir.inputSchema !== undefined &&
111
+ describeSchema(ir.inputSchema).singularType === "object",
112
+ requiresClientApply: ir.requiresClientApply,
106
113
  };
107
114
  }
108
115
  /** Render the typed invoker factory for one workflow. */
@@ -149,6 +156,65 @@ function renderFactory(wf) {
149
156
  ` ...opts,\n` +
150
157
  ` workflowKey: ${keyLiteral},\n` +
151
158
  ` }),`);
159
+ // Typed termination (#1543), emitted for EVERY non-reserved workflow — any
160
+ // run can be terminated. `output` is bound to `<Key>Output` (a terminated run
161
+ // can carry partial output), same schema-less → `unknown` rule as getStatus.
162
+ // `runKey` stays required. Same spread-first / key-last ordering so an untyped
163
+ // caller cannot override the key and terminate the wrong workflow's run. This
164
+ // member reverses #1512's start/status-only decision.
165
+ lines.push(` terminate: (opts: Omit<TerminateWorkflowOptions, "workflowKey">) =>\n` +
166
+ ` client.workflows.terminate<${wf.outputTypeName}>({\n` +
167
+ ` ...opts,\n` +
168
+ ` workflowKey: ${keyLiteral},\n` +
169
+ ` }),`);
170
+ // Typed apply handler (#1543 Phase 2), emitted ONLY for an apply-mode
171
+ // workflow (`requiresClientApply !== false` — the same default the server
172
+ // uses to decide a completed run needs a client apply). `WorkflowDefineOptions<O>`
173
+ // and `WorkflowApplyHandler<O>` come from #1542; binding `<Key>Output` here
174
+ // makes the `onApply` callback's `output` the workflow's real output type
175
+ // instead of `any`. The key is passed as a positional argument, so there is
176
+ // no spread a caller could use to override it.
177
+ //
178
+ // No `getPendingApplies` member: #1542 shipped it as a concrete
179
+ // `PendingApply[]` (the server element carries no `output`, so there is no
180
+ // output generic to bind), and the call is scoped to a DOCUMENT rather than
181
+ // a workflow — a per-workflow member would bind nothing and would wrongly
182
+ // suggest the result only covers this workflow.
183
+ if (wf.requiresClientApply) {
184
+ lines.push(` define: (options: WorkflowDefineOptions<${wf.outputTypeName}>) =>\n` +
185
+ ` client.workflows.define<${wf.outputTypeName}>(${keyLiteral}, options),`);
186
+ }
187
+ // Typed cron-trigger create/update (#1543), folded INTO the factory (not a
188
+ // separate export). The client's own `rootInput` generic (#1542) is bound to
189
+ // `<Key>Input`: `Partial<...>` for an object-shaped schema (an `inputMapping`
190
+ // may supply the rest), the full type otherwise. Binding the generic rather
191
+ // than restating the field keeps the caller-facing params in step with the
192
+ // client contract — notably `update` accepts `rootInput: null` to clear a
193
+ // stored root input, while `create` does not (the server rejects a null there
194
+ // with a 400). `workflowKey` is Omit-ted from the caller-facing params and
195
+ // pinned LAST so a caller can't redirect the trigger at another workflow.
196
+ // Nothing is spread after `workflowKey` — `rootInput` rides in via `...params`
197
+ // — preserving the key-last convention.
198
+ const rootInputType = wf.inputIsObjectShaped
199
+ ? `Partial<${wf.inputTypeName}>`
200
+ : wf.inputTypeName;
201
+ lines.push(` cronTriggers: {\n` +
202
+ ` create: (\n` +
203
+ ` params: Omit<CreateCronTriggerParams<${rootInputType}>, "workflowKey">\n` +
204
+ ` ) =>\n` +
205
+ ` client.cronTriggers.create<${rootInputType}>({\n` +
206
+ ` ...params,\n` +
207
+ ` workflowKey: ${keyLiteral},\n` +
208
+ ` }),\n` +
209
+ ` update: (\n` +
210
+ ` triggerId: string,\n` +
211
+ ` params: Omit<UpdateCronTriggerParams<${rootInputType}>, "workflowKey">\n` +
212
+ ` ) =>\n` +
213
+ ` client.cronTriggers.update<${rootInputType}>(triggerId, {\n` +
214
+ ` ...params,\n` +
215
+ ` workflowKey: ${keyLiteral},\n` +
216
+ ` }),\n` +
217
+ ` },`);
152
218
  lines.push(` };`, `}`);
153
219
  return lines.join("\n");
154
220
  }
@@ -163,15 +229,24 @@ export function renderWorkflowFile(input) {
163
229
  return null;
164
230
  const fingerprint = fingerprintToml(input.tomlContent);
165
231
  // Only import the option types actually referenced, so the generated file
166
- // compiles under a consumer's `noUnusedLocals`. `GetWorkflowStatusOptions` is
167
- // always referenced now every factory emits a `getStatus` member (#1512).
232
+ // compiles under a consumer's `noUnusedLocals`. `GetWorkflowStatusOptions`,
233
+ // `TerminateWorkflowOptions`, and the two cron-param types are always
234
+ // referenced now — every factory emits `getStatus`, `terminate` (#1512 /
235
+ // #1543), and `cronTriggers.{create,update}` (#1543).
168
236
  const imports = [
237
+ "CreateCronTriggerParams",
169
238
  "GetWorkflowStatusOptions",
170
239
  "JsBaoClient",
171
240
  "StartWorkflowOptions",
241
+ "TerminateWorkflowOptions",
242
+ "UpdateCronTriggerParams",
172
243
  ];
173
244
  if (wf.syncCallable)
174
245
  imports.push("RunSyncWorkflowOptions");
246
+ // Only an apply-mode workflow emits `define`, so its option type is imported
247
+ // only then — a non-apply-mode file would otherwise fail `noUnusedLocals`.
248
+ if (wf.requiresClientApply)
249
+ imports.push("WorkflowDefineOptions");
175
250
  imports.sort();
176
251
  const blocks = [];
177
252
  blocks.push([