primitive-admin 1.1.0-alpha.42 → 1.1.0-alpha.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/commands/catalog.js +3 -3
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/databases.js +157 -25
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/guides.d.ts +86 -0
- package/dist/src/commands/guides.js +222 -41
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/integrations.js +2 -2
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/prompts.js +2 -2
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/sync.d.ts +23 -3
- package/dist/src/commands/sync.js +257 -93
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/workflows.js +6 -6
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +12 -1
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.js +5 -5
- package/dist/src/lib/block-layout.js.map +1 -1
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/constants.d.ts +9 -0
- package/dist/src/lib/constants.js +9 -0
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/csv.d.ts +1 -2
- package/dist/src/lib/csv.js +3 -8
- package/dist/src/lib/csv.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +5 -2
- package/dist/src/lib/db-codegen/dbGenerator.js +204 -7
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +29 -0
- package/dist/src/lib/db-codegen/dbNaming.js +58 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +125 -4
- package/dist/src/lib/db-codegen/dbTemplates.js +183 -16
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +2 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/init-config.js +2 -2
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/toml-database-config.d.ts +10 -39
- package/dist/src/lib/toml-database-config.js +16 -82
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +1 -1
- package/dist/src/lib/toml-metadata-config.js +3 -3
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +3 -3
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +1 -1
- package/dist/src/lib/workflow-toml-validator.js +1 -1
- package/dist/src/validators.d.ts +4 -3
- package/dist/src/validators.js +6 -5
- package/dist/src/validators.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
|
-
import * as TOML from "@iarna/toml";
|
|
5
4
|
import { lookup as mimeLookup } from "mime-types";
|
|
6
5
|
import { ApiClient, ApiError, ConflictError, SchemaRequiredError, OperationRefError, SchemaBreaksOpsError, SchemaHasUncheckableOpsError, TomlParseError, OpsExistError, } from "../lib/api-client.js";
|
|
7
6
|
import { buildDatabaseTypeTomlData, detectExistingOperationForms, normalizeOperationFromToml, normalizeSubscriptionFromToml, SubscriptionAccessKeyConflictError, } from "../lib/toml-database-config.js";
|
|
7
|
+
import { canEmitNative } from "../lib/toml-native-form.js";
|
|
8
8
|
import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMetadataCategoryConfig, parseDeclaredAccessManifestToml, validateDeclaredAccessManifest, serializeDeclaredAccessManifest, } from "../lib/toml-metadata-config.js";
|
|
9
9
|
import { validateOperations, formatIssue, } from "../lib/toml-params-validator.js";
|
|
10
10
|
import { getServerUrl, resolveAppId } from "../lib/config.js";
|
|
@@ -12,6 +12,7 @@ import { resolveSyncDir, resolveSnapshotsRoot, isAutoResolvedSyncDir, checkLegac
|
|
|
12
12
|
import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneSnapshots, } from "../lib/snapshots.js";
|
|
13
13
|
import { validateWorkflowToml, formatWorkflowTomlErrors, } from "../lib/workflow-toml-validator.js";
|
|
14
14
|
import { expandWorkflowTomlData } from "../lib/workflow-fragments.js";
|
|
15
|
+
import { parseConfigToml, stringifyConfigToml } from "../lib/config-toml.js";
|
|
15
16
|
import { applyWorkflowBody } from "../lib/workflow-apply.js";
|
|
16
17
|
import { buildWorkflowPayloadFromToml } from "../lib/workflow-payload.js";
|
|
17
18
|
import { detectLayout, migrateLegacyToV2 } from "../lib/block-layout.js";
|
|
@@ -190,6 +191,25 @@ export function normalizeWorkflowTomlDefaults(parsed) {
|
|
|
190
191
|
workflow[field] = defaultValue;
|
|
191
192
|
}
|
|
192
193
|
}
|
|
194
|
+
// Schema-form canonicalization (issue #1446): a legacy file carries
|
|
195
|
+
// `inputSchema` / `outputSchema` as a JSON string, while the native form (and
|
|
196
|
+
// the remote serialization) carries a parsed object. Parse the string form to
|
|
197
|
+
// its object here so the two forms hash equal — `sync diff` stays
|
|
198
|
+
// form-independent and an unchanged schema never shows a false `modified`
|
|
199
|
+
// during the transition. This runs only on the diff-hash path
|
|
200
|
+
// (`hashWorkflowTomlForDiff`); the stored-state hash path is untouched, so
|
|
201
|
+
// there is no forced one-time re-push after upgrade.
|
|
202
|
+
for (const field of ["inputSchema", "outputSchema"]) {
|
|
203
|
+
if (typeof workflow[field] === "string") {
|
|
204
|
+
try {
|
|
205
|
+
workflow[field] = JSON.parse(workflow[field]);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Not valid JSON — leave the raw string so a genuinely malformed value
|
|
209
|
+
// still compares by its literal content.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
193
213
|
return { ...parsed, workflow };
|
|
194
214
|
}
|
|
195
215
|
/**
|
|
@@ -208,12 +228,14 @@ export function hashWorkflowTomlForDiff(parsed) {
|
|
|
208
228
|
* `getWorkflow` + active `getWorkflowConfig`), mirroring exactly what a fresh
|
|
209
229
|
* `sync pull` would write to disk. Serializes via `serializeWorkflow`, parses
|
|
210
230
|
* the resulting TOML (remote serialized TOML never carries `include`s, so a
|
|
211
|
-
* plain `
|
|
231
|
+
* plain `parseConfigToml` is sufficient — no fragment path needed), then hashes
|
|
212
232
|
* through `hashWorkflowTomlForDiff` so it lines up with the local-file hash.
|
|
213
233
|
*/
|
|
214
234
|
export function hashRemoteWorkflowForDiff(workflow, draft, configs) {
|
|
215
235
|
const serialized = serializeWorkflow(workflow, draft, configs || []);
|
|
216
|
-
|
|
236
|
+
// serializeWorkflow emits via smol-toml (issue #1450) and may contain a
|
|
237
|
+
// heterogeneous array, so re-parse through the TOML 1.0-compliant facade.
|
|
238
|
+
const parsed = parseConfigToml(serialized);
|
|
217
239
|
return hashWorkflowTomlForDiff(parsed);
|
|
218
240
|
}
|
|
219
241
|
// Boolean [auth] keys in app.toml that map 1:1 to app-settings fields.
|
|
@@ -238,7 +260,7 @@ const RECOGNIZED_AUTH_KEYS = new Set([
|
|
|
238
260
|
*
|
|
239
261
|
* Apple/OTP fields round-trip alongside the legacy google/passkey/magic-link
|
|
240
262
|
* keys. Two asymmetries are handled deliberately:
|
|
241
|
-
* - `appleSignInEnabled` comes back as `boolean | null`;
|
|
263
|
+
* - `appleSignInEnabled` comes back as `boolean | null`; smol-toml throws on
|
|
242
264
|
* null, so an unset value (null) is omitted rather than written.
|
|
243
265
|
* - `appleAudiences` is omitted when empty. An empty array and an omitted key
|
|
244
266
|
* both mean "no audiences" (the server normalizes [] -> null), so omitting
|
|
@@ -317,7 +339,7 @@ function serializeAppSettings(settings) {
|
|
|
317
339
|
maxAge: settings.corsMaxAge,
|
|
318
340
|
};
|
|
319
341
|
}
|
|
320
|
-
return
|
|
342
|
+
return stringifyConfigToml(data);
|
|
321
343
|
}
|
|
322
344
|
function serializeIntegration(integration) {
|
|
323
345
|
const data = {
|
|
@@ -330,9 +352,9 @@ function serializeIntegration(integration) {
|
|
|
330
352
|
},
|
|
331
353
|
requestConfig: integration.requestConfig || {},
|
|
332
354
|
};
|
|
333
|
-
return
|
|
355
|
+
return stringifyConfigToml(data);
|
|
334
356
|
}
|
|
335
|
-
function serializeWebhook(webhook) {
|
|
357
|
+
export function serializeWebhook(webhook) {
|
|
336
358
|
const data = {
|
|
337
359
|
webhook: {
|
|
338
360
|
key: webhook.webhookKey,
|
|
@@ -353,7 +375,9 @@ function serializeWebhook(webhook) {
|
|
|
353
375
|
// the template shape here too so a masked or raw value can never be written
|
|
354
376
|
// to TOML.
|
|
355
377
|
if (typeof webhook.signingSecret === "string" &&
|
|
356
|
-
|
|
378
|
+
// Whitespace-tolerant (#1468) to match the server's shared resolver, so a
|
|
379
|
+
// hand-authored spaced reference `{{ secrets.KEY }}` round-trips pull→push.
|
|
380
|
+
/\{\{\s*secrets\.[A-Z][A-Z0-9_]{0,63}\s*\}\}/.test(webhook.signingSecret)) {
|
|
357
381
|
data.webhook.signingSecret = webhook.signingSecret;
|
|
358
382
|
}
|
|
359
383
|
// Parse JSON fields if they're strings
|
|
@@ -384,7 +408,7 @@ function serializeWebhook(webhook) {
|
|
|
384
408
|
if (data.webhook[k] === undefined)
|
|
385
409
|
delete data.webhook[k];
|
|
386
410
|
});
|
|
387
|
-
return
|
|
411
|
+
return stringifyConfigToml(data);
|
|
388
412
|
}
|
|
389
413
|
function serializeCronTrigger(trigger) {
|
|
390
414
|
const data = {
|
|
@@ -420,7 +444,7 @@ function serializeCronTrigger(trigger) {
|
|
|
420
444
|
if (data.cronTrigger[k] === undefined)
|
|
421
445
|
delete data.cronTrigger[k];
|
|
422
446
|
});
|
|
423
|
-
return
|
|
447
|
+
return stringifyConfigToml(data);
|
|
424
448
|
}
|
|
425
449
|
function serializeBlobBucket(bucket) {
|
|
426
450
|
const data = {
|
|
@@ -446,7 +470,7 @@ function serializeBlobBucket(bucket) {
|
|
|
446
470
|
if (data.bucket[k] === undefined)
|
|
447
471
|
delete data.bucket[k];
|
|
448
472
|
});
|
|
449
|
-
return
|
|
473
|
+
return stringifyConfigToml(data);
|
|
450
474
|
}
|
|
451
475
|
function serializePrompt(prompt) {
|
|
452
476
|
const data = {
|
|
@@ -469,9 +493,102 @@ function serializePrompt(prompt) {
|
|
|
469
493
|
userPromptTemplate: config.userPromptTemplate,
|
|
470
494
|
})),
|
|
471
495
|
};
|
|
472
|
-
return
|
|
496
|
+
return stringifyConfigToml(data);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Emit a workflow schema field (`inputSchema` / `outputSchema`) for the TOML
|
|
500
|
+
* `[workflow]` table.
|
|
501
|
+
*
|
|
502
|
+
* Issue #1446: pull used to `JSON.stringify` both schema fields into a single
|
|
503
|
+
* escaped-JSON line — unreadable and undiffable. We now emit the parsed JSON
|
|
504
|
+
* Schema as a native `[workflow.inputSchema]` / `[workflow.outputSchema]`
|
|
505
|
+
* table whenever it round-trips faithfully (`canEmitNative === null`), falling
|
|
506
|
+
* back to the JSON string only for the values TOML silently drops (`null` /
|
|
507
|
+
* `undefined` anywhere in the schema). The empty-object schema `{}` is a real,
|
|
508
|
+
* distinct value ("accepts anything") and emits as an empty table.
|
|
509
|
+
*
|
|
510
|
+
* Returns `undefined` for an absent schema (the key is then omitted). The
|
|
511
|
+
* sponsor-approved decision is to always move to native aggressively — no
|
|
512
|
+
* form preservation — so this does not consult the existing file's form.
|
|
513
|
+
*/
|
|
514
|
+
function serializeWorkflowSchemaField(value, fieldName, workflowKey, log) {
|
|
515
|
+
if (value === undefined || value === null)
|
|
516
|
+
return undefined;
|
|
517
|
+
// Schemas arrive from the server GET as parsed objects; defensively accept a
|
|
518
|
+
// JSON string too (e.g. an inline-authored file re-serialized here).
|
|
519
|
+
let obj = value;
|
|
520
|
+
if (typeof value === "string") {
|
|
521
|
+
try {
|
|
522
|
+
obj = JSON.parse(value);
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// Not valid JSON — keep the original string as the emitted value.
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (obj && typeof obj === "object") {
|
|
530
|
+
const reason = canEmitNative(obj);
|
|
531
|
+
if (reason) {
|
|
532
|
+
log(`Workflow '${workflowKey}': cannot emit \`${fieldName}\` as native TOML (${reason}); using JSON string.`);
|
|
533
|
+
return JSON.stringify(obj);
|
|
534
|
+
}
|
|
535
|
+
return obj;
|
|
536
|
+
}
|
|
537
|
+
// A non-object schema (unexpected) — preserve it as a JSON string.
|
|
538
|
+
return JSON.stringify(obj);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Rewrite a workflow TOML file's string-form `inputSchema` / `outputSchema` to
|
|
542
|
+
* native object form for `sync migrate-toml` (issue #1446).
|
|
543
|
+
*
|
|
544
|
+
* Purely local, no server fetch. Parses the raw source WITHOUT expanding
|
|
545
|
+
* fragment `include`s (so those directives survive the rewrite), swaps a
|
|
546
|
+
* JSON-string schema for its parsed object when it round-trips faithfully
|
|
547
|
+
* (`canEmitNative === null`), and re-serializes via `stringifyConfigToml`.
|
|
548
|
+
*
|
|
549
|
+
* Only rewrites when a field is actually converted: a file whose schemas are
|
|
550
|
+
* already native, absent, or un-representable (a `null` default) is returned
|
|
551
|
+
* with `changed: false` and its original bytes untouched — so migrate is
|
|
552
|
+
* idempotent and never reports a spurious migration or reformats a file it
|
|
553
|
+
* didn't need to touch.
|
|
554
|
+
*/
|
|
555
|
+
export function rewriteWorkflowSchemasToNative(rawToml, workflowKey = "", log = () => { }) {
|
|
556
|
+
const parsed = parseConfigToml(rawToml);
|
|
557
|
+
if (!parsed || typeof parsed !== "object" || !parsed.workflow) {
|
|
558
|
+
return { content: rawToml, changed: false };
|
|
559
|
+
}
|
|
560
|
+
const workflow = parsed.workflow;
|
|
561
|
+
let converted = false;
|
|
562
|
+
for (const field of ["inputSchema", "outputSchema"]) {
|
|
563
|
+
const value = workflow[field];
|
|
564
|
+
// Only a JSON-string schema is a migration candidate; an object is already
|
|
565
|
+
// native and an absent key needs nothing.
|
|
566
|
+
if (typeof value !== "string")
|
|
567
|
+
continue;
|
|
568
|
+
let obj;
|
|
569
|
+
try {
|
|
570
|
+
obj = JSON.parse(value);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// Not JSON — leave the literal string in place.
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (!obj || typeof obj !== "object")
|
|
577
|
+
continue;
|
|
578
|
+
const reason = canEmitNative(obj);
|
|
579
|
+
if (reason) {
|
|
580
|
+
log(`Workflow '${workflowKey}': cannot migrate \`${field}\` to native TOML (${reason}); leaving as JSON string.`);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
workflow[field] = obj;
|
|
584
|
+
converted = true;
|
|
585
|
+
}
|
|
586
|
+
if (!converted)
|
|
587
|
+
return { content: rawToml, changed: false };
|
|
588
|
+
return { content: stringifyConfigToml(parsed), changed: true };
|
|
473
589
|
}
|
|
474
|
-
export function serializeWorkflow(workflow, draft, configs) {
|
|
590
|
+
export function serializeWorkflow(workflow, draft, configs, logger) {
|
|
591
|
+
const log = logger ?? (() => { });
|
|
475
592
|
// Find the active config or use the first one
|
|
476
593
|
const activeConfigId = workflow.activeConfigId;
|
|
477
594
|
const activeConfig = configs?.find((c) => c.configId === activeConfigId) || configs?.[0];
|
|
@@ -516,9 +633,11 @@ export function serializeWorkflow(workflow, draft, configs) {
|
|
|
516
633
|
// they're safe to coerce here.
|
|
517
634
|
requiresClientApply: workflow.requiresClientApply !== false,
|
|
518
635
|
syncCallable: workflow.syncCallable === true,
|
|
519
|
-
// Schemas are at workflow level
|
|
520
|
-
|
|
521
|
-
|
|
636
|
+
// Schemas are at workflow level. Emit native `[workflow.inputSchema]` /
|
|
637
|
+
// `[workflow.outputSchema]` tables when representable, JSON string only
|
|
638
|
+
// as a data-loss fallback (issue #1446).
|
|
639
|
+
inputSchema: serializeWorkflowSchemaField(workflow.inputSchema, "inputSchema", workflow.workflowKey, log),
|
|
640
|
+
outputSchema: serializeWorkflowSchemaField(workflow.outputSchema, "outputSchema", workflow.workflowKey, log),
|
|
522
641
|
},
|
|
523
642
|
// Use active config steps, fall back to draft
|
|
524
643
|
steps,
|
|
@@ -534,7 +653,7 @@ export function serializeWorkflow(workflow, draft, configs) {
|
|
|
534
653
|
// a workflow's `md`/`secrets` declarations round-trip through pull → push.
|
|
535
654
|
...serializeDeclaredAccessManifest(workflow.metadataManifest),
|
|
536
655
|
};
|
|
537
|
-
return
|
|
656
|
+
return stringifyConfigToml(data);
|
|
538
657
|
}
|
|
539
658
|
// Database config serialization helpers
|
|
540
659
|
//
|
|
@@ -543,15 +662,17 @@ export function serializeWorkflow(workflow, draft, configs) {
|
|
|
543
662
|
// rows over JSON-strings stuffed into a single field. The form is sticky
|
|
544
663
|
// per file — if the existing file uses the legacy JSON-string form, we
|
|
545
664
|
// preserve it so we don't generate surprise diffs. New files default to
|
|
546
|
-
// native.
|
|
547
|
-
//
|
|
665
|
+
// native. The round-trip serializes with `smol-toml` (TOML 1.0), so
|
|
666
|
+
// heterogeneous arrays emit natively (issue #1450); only shapes that TOML
|
|
667
|
+
// genuinely can't represent without data loss (null / undefined / an array
|
|
668
|
+
// containing null) fall back to a JSON string per field with a log message.
|
|
548
669
|
//
|
|
549
670
|
// Issue #666: when the type has a stored `schema` (raw TOML of `[models.*]`
|
|
550
671
|
// blocks), `buildDatabaseTypeTomlData` parses + merges it into the same TOML
|
|
551
672
|
// object so the result round-trips as `[models.<Name>.fields.<field>]`.
|
|
552
673
|
function serializeDatabaseType(typeConfig, operations, ruleSetIdToName, options = {}) {
|
|
553
674
|
const data = buildDatabaseTypeTomlData(typeConfig, operations, ruleSetIdToName, options);
|
|
554
|
-
return
|
|
675
|
+
return stringifyConfigToml(data);
|
|
555
676
|
}
|
|
556
677
|
function serializeEmailTemplate(template) {
|
|
557
678
|
// The API returns { emailType, hasOverride, override: { subject, htmlBody, textBody }, default: { ... } }
|
|
@@ -569,7 +690,7 @@ function serializeEmailTemplate(template) {
|
|
|
569
690
|
if (override.textBody) {
|
|
570
691
|
data.template.textBody = override.textBody;
|
|
571
692
|
}
|
|
572
|
-
return
|
|
693
|
+
return stringifyConfigToml(data);
|
|
573
694
|
}
|
|
574
695
|
function serializeRuleSet(ruleSet) {
|
|
575
696
|
const data = {
|
|
@@ -586,7 +707,7 @@ function serializeRuleSet(ruleSet) {
|
|
|
586
707
|
if (Object.keys(rules).length > 0) {
|
|
587
708
|
data.rules = rules;
|
|
588
709
|
}
|
|
589
|
-
return
|
|
710
|
+
return stringifyConfigToml(data);
|
|
590
711
|
}
|
|
591
712
|
export function serializeGroupTypeConfig(config, ruleSetIdToName) {
|
|
592
713
|
const ruleSetName = config.ruleSetId ? (ruleSetIdToName.get(config.ruleSetId) || "") : "";
|
|
@@ -601,7 +722,7 @@ export function serializeGroupTypeConfig(config, ruleSetIdToName) {
|
|
|
601
722
|
// round-trips, exactly like the collection/database/workflow type configs.
|
|
602
723
|
...serializeDeclaredAccessManifest(config.metadataManifest),
|
|
603
724
|
};
|
|
604
|
-
return
|
|
725
|
+
return stringifyConfigToml(data);
|
|
605
726
|
}
|
|
606
727
|
function serializeCollectionTypeConfig(config, ruleSetIdToName) {
|
|
607
728
|
const ruleSetName = config.ruleSetId ? (ruleSetIdToName.get(config.ruleSetId) || "") : "";
|
|
@@ -614,7 +735,7 @@ function serializeCollectionTypeConfig(config, ruleSetIdToName) {
|
|
|
614
735
|
// `[metadata]` + `secrets` fragments so a pull→push→pull cycle round-trips.
|
|
615
736
|
...serializeDeclaredAccessManifest(config.metadataManifest),
|
|
616
737
|
};
|
|
617
|
-
return
|
|
738
|
+
return stringifyConfigToml(data);
|
|
618
739
|
}
|
|
619
740
|
export function parseDatabaseTypeToml(tomlData) {
|
|
620
741
|
const typeSection = tomlData.type || {};
|
|
@@ -680,7 +801,7 @@ export function parseDatabaseTypeToml(tomlData) {
|
|
|
680
801
|
let schemaSerialized = null;
|
|
681
802
|
if (tomlData.models && typeof tomlData.models === "object") {
|
|
682
803
|
try {
|
|
683
|
-
schemaSerialized =
|
|
804
|
+
schemaSerialized = stringifyConfigToml({ models: tomlData.models });
|
|
684
805
|
}
|
|
685
806
|
catch {
|
|
686
807
|
// Fall through — server-side parser will fail with a clear
|
|
@@ -769,7 +890,7 @@ export function parseCollectionTypeConfigToml(tomlData) {
|
|
|
769
890
|
// the original object untouched.
|
|
770
891
|
function parseTomlFile(filePath) {
|
|
771
892
|
const content = readFileSync(filePath, "utf-8");
|
|
772
|
-
const parsed =
|
|
893
|
+
const parsed = parseConfigToml(content);
|
|
773
894
|
return expandWorkflowTomlData(parsed, filePath);
|
|
774
895
|
}
|
|
775
896
|
/**
|
|
@@ -861,7 +982,7 @@ export function serializeTestCase(testCase, lookupMaps) {
|
|
|
861
982
|
expectedJsonSubset: testCase.expectedJsonSubset || "{}",
|
|
862
983
|
},
|
|
863
984
|
};
|
|
864
|
-
return
|
|
985
|
+
return stringifyConfigToml(data);
|
|
865
986
|
}
|
|
866
987
|
export function parseTestCaseToml(tomlData) {
|
|
867
988
|
const test = tomlData.test || {};
|
|
@@ -1587,7 +1708,7 @@ Directory Structure:
|
|
|
1587
1708
|
for (const { workflow, draft, configs } of workflows) {
|
|
1588
1709
|
const filename = `${workflow.workflowKey}.toml`;
|
|
1589
1710
|
const filePath = join(configDir, "workflows", filename);
|
|
1590
|
-
writeFileSync(filePath, serializeWorkflow(workflow, draft, configs || []));
|
|
1711
|
+
writeFileSync(filePath, serializeWorkflow(workflow, draft, configs || [], (msg) => info(` ${msg}`)));
|
|
1591
1712
|
// Hash the expanded (post-fragment-splice) form so subsequent pushes
|
|
1592
1713
|
// can detect fragment-only edits. See `computeExpandedContentHash`.
|
|
1593
1714
|
// Pulled workflows never carry `include`, so the expander is a
|
|
@@ -5097,11 +5218,15 @@ Directory Structure:
|
|
|
5097
5218
|
// Migrate-toml (issue #752): bulk-rewrite database-type TOML files from
|
|
5098
5219
|
// the legacy JSON-string form for `definition`/`params` to the native
|
|
5099
5220
|
// nested-table form. Idempotent: ops already in native form are left as
|
|
5100
|
-
// is;
|
|
5101
|
-
//
|
|
5221
|
+
// is; the round-trip serializes with smol-toml (TOML 1.0, issue #1450), so
|
|
5222
|
+
// heterogeneous arrays now converge to native form too — only shapes that
|
|
5223
|
+
// TOML can't represent without data loss (null / undefined / an array
|
|
5224
|
+
// containing null) fall back to JSON-string per field with a log message.
|
|
5102
5225
|
sync
|
|
5103
5226
|
.command("migrate-toml")
|
|
5104
|
-
.description("Rewrite database-type TOML files to native
|
|
5227
|
+
.description("Rewrite database-type and workflow TOML files to native form " +
|
|
5228
|
+
"([operations.definition] / [[operations.params]] for database types; " +
|
|
5229
|
+
"[workflow.inputSchema] / [workflow.outputSchema] for workflows)")
|
|
5105
5230
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
5106
5231
|
.option("--app <app-id>", "App ID")
|
|
5107
5232
|
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
@@ -5120,83 +5245,122 @@ Directory Structure:
|
|
|
5120
5245
|
process.exit(1);
|
|
5121
5246
|
}
|
|
5122
5247
|
const dbTypesDir = join(configDir, "database-types");
|
|
5123
|
-
|
|
5124
|
-
|
|
5248
|
+
const workflowsDir = join(configDir, "workflows");
|
|
5249
|
+
const dbFiles = existsSync(dbTypesDir)
|
|
5250
|
+
? readdirSync(dbTypesDir).filter((f) => f.endsWith(".toml"))
|
|
5251
|
+
: [];
|
|
5252
|
+
const workflowFiles = existsSync(workflowsDir)
|
|
5253
|
+
? readdirSync(workflowsDir).filter((f) => f.endsWith(".toml"))
|
|
5254
|
+
: [];
|
|
5255
|
+
// Restructured early-return (issue #1446): don't bail just because
|
|
5256
|
+
// `database-types/` is absent — a workflow-only project still has
|
|
5257
|
+
// `inputSchema`/`outputSchema` to migrate.
|
|
5258
|
+
if (dbFiles.length === 0 && workflowFiles.length === 0) {
|
|
5259
|
+
info("No database-type or workflow TOML files found; nothing to migrate.");
|
|
5125
5260
|
return;
|
|
5126
5261
|
}
|
|
5127
|
-
const files = readdirSync(dbTypesDir).filter((f) => f.endsWith(".toml"));
|
|
5128
|
-
if (files.length === 0) {
|
|
5129
|
-
info("No database-type TOML files found; nothing to migrate.");
|
|
5130
|
-
return;
|
|
5131
|
-
}
|
|
5132
|
-
// Hydrate ruleSetIdToName from local sync state so files that
|
|
5133
|
-
// reference a rule set via the legacy `ruleSetId = "01..."` form
|
|
5134
|
-
// round-trip with the correct `ruleSetName`. Without this, any user
|
|
5135
|
-
// who runs migrate-toml against a file with a rule-set assignment
|
|
5136
|
-
// would silently lose that reference (review feedback r3246633010).
|
|
5137
|
-
//
|
|
5138
|
-
// Files that use the modern key-based `ruleSetName = "..."` form
|
|
5139
|
-
// don't need the map at all — `parseDatabaseTypeToml` stores the
|
|
5140
|
-
// value in `typeConfig._ruleSetName` and the serializer now prefers
|
|
5141
|
-
// that. The map is only load-bearing for the legacy ID-based form.
|
|
5142
|
-
const ruleSetIdToName = new Map();
|
|
5143
|
-
const migrateSyncState = loadSyncState(configDir);
|
|
5144
|
-
if (migrateSyncState?.entities?.ruleSets) {
|
|
5145
|
-
for (const [fileKey, entry] of Object.entries(migrateSyncState.entities.ruleSets)) {
|
|
5146
|
-
if (entry && typeof entry === "object" && "id" in entry && entry.id) {
|
|
5147
|
-
// fileKey is the sanitized rule-set name (see sync pull at
|
|
5148
|
-
// sync.ts:1385). It's the same shape the server returned and
|
|
5149
|
-
// matches what a TOML file's ruleSetName field would carry.
|
|
5150
|
-
ruleSetIdToName.set(entry.id, fileKey);
|
|
5151
|
-
}
|
|
5152
|
-
}
|
|
5153
|
-
}
|
|
5154
5262
|
let migratedCount = 0;
|
|
5155
5263
|
let unchangedCount = 0;
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
//
|
|
5161
|
-
//
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5264
|
+
// ---- database-types/ pass ----
|
|
5265
|
+
if (dbFiles.length > 0) {
|
|
5266
|
+
// Hydrate ruleSetIdToName from local sync state so files that
|
|
5267
|
+
// reference a rule set via the legacy `ruleSetId = "01..."` form
|
|
5268
|
+
// round-trip with the correct `ruleSetName`. Without this, any user
|
|
5269
|
+
// who runs migrate-toml against a file with a rule-set assignment
|
|
5270
|
+
// would silently lose that reference (review feedback r3246633010).
|
|
5271
|
+
//
|
|
5272
|
+
// Files that use the modern key-based `ruleSetName = "..."` form
|
|
5273
|
+
// don't need the map at all — `parseDatabaseTypeToml` stores the
|
|
5274
|
+
// value in `typeConfig._ruleSetName` and the serializer now prefers
|
|
5275
|
+
// that. The map is only load-bearing for the legacy ID-based form.
|
|
5276
|
+
const ruleSetIdToName = new Map();
|
|
5277
|
+
const migrateSyncState = loadSyncState(configDir);
|
|
5278
|
+
if (migrateSyncState?.entities?.ruleSets) {
|
|
5279
|
+
for (const [fileKey, entry] of Object.entries(migrateSyncState.entities.ruleSets)) {
|
|
5280
|
+
if (entry && typeof entry === "object" && "id" in entry && entry.id) {
|
|
5281
|
+
// fileKey is the sanitized rule-set name (see sync pull at
|
|
5282
|
+
// sync.ts:1385). It's the same shape the server returned and
|
|
5283
|
+
// matches what a TOML file's ruleSetName field would carry.
|
|
5284
|
+
ruleSetIdToName.set(entry.id, fileKey);
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5168
5287
|
}
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5288
|
+
for (const file of dbFiles) {
|
|
5289
|
+
const filePath = join(dbTypesDir, file);
|
|
5290
|
+
const rawBefore = readFileSync(filePath, "utf-8");
|
|
5291
|
+
const tomlData = parseTomlFile(filePath);
|
|
5292
|
+
// `accessRule`/`access` conflicts surface as a clear error here too,
|
|
5293
|
+
// rather than silently dropping the field during a migrate rewrite.
|
|
5294
|
+
let typeConfig;
|
|
5295
|
+
let operations;
|
|
5296
|
+
let subscriptions;
|
|
5297
|
+
try {
|
|
5298
|
+
({ typeConfig, operations, subscriptions } =
|
|
5299
|
+
parseDatabaseTypeToml(tomlData));
|
|
5174
5300
|
}
|
|
5175
|
-
|
|
5301
|
+
catch (err) {
|
|
5302
|
+
if (err instanceof SubscriptionAccessKeyConflictError) {
|
|
5303
|
+
error(` ${file}: ${err.message}`);
|
|
5304
|
+
error(`Aborting migrate-toml: subscription \`access\`/\`accessRule\` conflict in ${file}.`);
|
|
5305
|
+
process.exit(1);
|
|
5306
|
+
}
|
|
5307
|
+
throw err;
|
|
5308
|
+
}
|
|
5309
|
+
// Force-native: ignore existing hints so every TOMLable field
|
|
5310
|
+
// ends up in nested-table form. Subscriptions are preserved on the
|
|
5311
|
+
// rewrite (issue #803) so migrate-toml never drops them.
|
|
5312
|
+
const rewritten = serializeDatabaseType(typeConfig, operations, ruleSetIdToName, {
|
|
5313
|
+
defaultForm: "native",
|
|
5314
|
+
logger: (msg) => info(` ${msg}`),
|
|
5315
|
+
subscriptions,
|
|
5316
|
+
});
|
|
5317
|
+
if (rewritten === rawBefore) {
|
|
5318
|
+
unchangedCount++;
|
|
5319
|
+
continue;
|
|
5320
|
+
}
|
|
5321
|
+
if (options.dryRun) {
|
|
5322
|
+
info(`Would migrate database-types/${file}`);
|
|
5323
|
+
}
|
|
5324
|
+
else {
|
|
5325
|
+
writeFileSync(filePath, rewritten);
|
|
5326
|
+
info(`Migrated database-types/${file}`);
|
|
5327
|
+
// Update content hash in sync state if we have an entry for it.
|
|
5328
|
+
const syncState = loadSyncState(configDir);
|
|
5329
|
+
const dbType = typeConfig.databaseType || basename(file, ".toml");
|
|
5330
|
+
if (syncState?.entities?.databaseTypes?.[dbType]) {
|
|
5331
|
+
syncState.entities.databaseTypes[dbType].contentHash = computeFileHash(filePath);
|
|
5332
|
+
saveSyncState(configDir, syncState);
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
migratedCount++;
|
|
5176
5336
|
}
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5337
|
+
}
|
|
5338
|
+
// ---- workflows/ pass (issue #1446) ----
|
|
5339
|
+
// Convert string-form `inputSchema`/`outputSchema` to native tables.
|
|
5340
|
+
// Purely local, mirroring the database-types pass. The stored workflow
|
|
5341
|
+
// hash uses `computeExpandedContentHash` (parse then hash), NOT
|
|
5342
|
+
// `computeFileHash`, matching the workflow convention on the pull/push
|
|
5343
|
+
// paths — otherwise a push after migrate would spuriously re-push.
|
|
5344
|
+
for (const file of workflowFiles) {
|
|
5345
|
+
const filePath = join(workflowsDir, file);
|
|
5346
|
+
const rawBefore = readFileSync(filePath, "utf-8");
|
|
5347
|
+
const workflowKey = basename(file, ".toml");
|
|
5348
|
+
const { content: rewritten, changed } = rewriteWorkflowSchemasToNative(rawBefore, workflowKey, (msg) => info(` ${msg}`));
|
|
5349
|
+
if (!changed) {
|
|
5186
5350
|
unchangedCount++;
|
|
5187
5351
|
continue;
|
|
5188
5352
|
}
|
|
5189
5353
|
if (options.dryRun) {
|
|
5190
|
-
info(`Would migrate
|
|
5354
|
+
info(`Would migrate workflows/${file}`);
|
|
5191
5355
|
}
|
|
5192
5356
|
else {
|
|
5193
5357
|
writeFileSync(filePath, rewritten);
|
|
5194
|
-
info(`Migrated
|
|
5195
|
-
// Update content hash
|
|
5358
|
+
info(`Migrated workflows/${file}`);
|
|
5359
|
+
// Update the stored content hash so a subsequent push sees a no-op.
|
|
5196
5360
|
const syncState = loadSyncState(configDir);
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5361
|
+
if (syncState?.entities?.workflows?.[workflowKey]) {
|
|
5362
|
+
syncState.entities.workflows[workflowKey].contentHash =
|
|
5363
|
+
computeExpandedContentHash(parseTomlFile(filePath));
|
|
5200
5364
|
saveSyncState(configDir, syncState);
|
|
5201
5365
|
}
|
|
5202
5366
|
}
|