@fmaplabs/meta-manifest 0.2.0 → 0.4.0

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.
@@ -41,12 +41,14 @@ var PULL_DEFINITION_QUERY = `query PullMetaobjectDefinition($type: String!) {
41
41
  required
42
42
  type { name }
43
43
  validations { name value }
44
+ capabilities { adminFilterable { enabled } }
44
45
  }
45
- access { admin storefront }
46
+ access { admin storefront customerAccount }
46
47
  capabilities {
47
48
  publishable { enabled }
48
49
  translatable { enabled }
49
- renderable { enabled }
50
+ renderable { enabled data { metaTitleKey metaDescriptionKey } }
51
+ onlineStore { enabled data { urlHandle } }
50
52
  }
51
53
  }
52
54
  }`;
@@ -56,6 +58,8 @@ var LIST_DEFINITIONS_QUERY = `query ListMetaobjectDefinitions($after: String) {
56
58
  id
57
59
  name
58
60
  type
61
+ description
62
+ displayNameKey
59
63
  fieldDefinitions {
60
64
  key
61
65
  name
@@ -63,6 +67,14 @@ var LIST_DEFINITIONS_QUERY = `query ListMetaobjectDefinitions($after: String) {
63
67
  required
64
68
  type { name }
65
69
  validations { name value }
70
+ capabilities { adminFilterable { enabled } }
71
+ }
72
+ access { admin storefront customerAccount }
73
+ capabilities {
74
+ publishable { enabled }
75
+ translatable { enabled }
76
+ renderable { enabled data { metaTitleKey metaDescriptionKey } }
77
+ onlineStore { enabled data { urlHandle } }
66
78
  }
67
79
  }
68
80
  pageInfo { hasNextPage endCursor }
@@ -205,6 +217,12 @@ async function runInit(opts = {}) {
205
217
 
206
218
  // src/codegen.ts
207
219
  var APP_PREFIX = "$app:";
220
+ function isMerchant(type) {
221
+ return !type.startsWith(APP_PREFIX);
222
+ }
223
+ function filterableEntry(field) {
224
+ return field.filterable ? ["filterable: true"] : [];
225
+ }
208
226
  var SIMPLE = {
209
227
  single_line_text_field: "text",
210
228
  multi_line_text_field: "multilineText",
@@ -248,6 +266,27 @@ function refTarget(field) {
248
266
  }
249
267
  return void 0;
250
268
  }
269
+ function refTargets(field) {
270
+ const many = v(field.validations, "metaobject_definition_types");
271
+ if (many) {
272
+ try {
273
+ const arr = JSON.parse(many);
274
+ if (Array.isArray(arr)) return arr.map(String);
275
+ } catch {
276
+ }
277
+ }
278
+ const single = v(field.validations, "metaobject_definition_type");
279
+ return single ? [single] : [];
280
+ }
281
+ function mixedTargetsLiteral(field, typeToIdent, warnings) {
282
+ const targets = refTargets(field);
283
+ const idents = targets.map((t) => typeToIdent.get(t));
284
+ if (!targets.length || idents.some((i) => !i)) {
285
+ warnings.push(`unresolved mixed reference on field "${field.key}"`);
286
+ return void 0;
287
+ }
288
+ return `[${idents.map((i) => `() => ${i}`).join(", ")}]`;
289
+ }
251
290
  function optsLiteral(entries) {
252
291
  return entries.length ? `{ ${entries.join(", ")} }` : "";
253
292
  }
@@ -286,8 +325,8 @@ function scalarEntries(field, warnings, builder) {
286
325
  jsonArr("file_type_options", "accept");
287
326
  return e;
288
327
  }
289
- function scalarCall(builder, field, warnings) {
290
- const lit = optsLiteral(scalarEntries(field, warnings, builder));
328
+ function scalarCall(builder, field, warnings, extra = []) {
329
+ const lit = optsLiteral([...scalarEntries(field, warnings, builder), ...extra]);
291
330
  return lit ? `m.${builder}(${lit})` : `m.${builder}()`;
292
331
  }
293
332
  function fieldCall(field, typeToIdent, warnings) {
@@ -297,6 +336,7 @@ function fieldCall(field, typeToIdent, warnings) {
297
336
  const max = v(field.validations, "max");
298
337
  const e = [`min: ${Number(min ?? 1)}`, `max: ${Number(max ?? 5)}`];
299
338
  if (field.required) e.unshift("required: true");
339
+ e.push(...filterableEntry(field));
300
340
  if (min === void 0 || max === void 0) warnings.push(`rating field "${field.key}" missing min/max`);
301
341
  return `m.rating(${optsLiteral(e)})`;
302
342
  }
@@ -307,7 +347,20 @@ function fieldCall(field, typeToIdent, warnings) {
307
347
  warnings.push(`unresolved reference on field "${field.key}"`);
308
348
  return `m.json() /* TODO: unmapped reference */`;
309
349
  }
310
- return field.required ? `m.ref(() => ${ident}, { required: true })` : `m.ref(() => ${ident})`;
350
+ const refEntries = [];
351
+ if (field.required) refEntries.push("required: true");
352
+ refEntries.push(...filterableEntry(field));
353
+ const opts = optsLiteral(refEntries);
354
+ return opts ? `m.ref(() => ${ident}, ${opts})` : `m.ref(() => ${ident})`;
355
+ }
356
+ if (type === "mixed_reference") {
357
+ const arr = mixedTargetsLiteral(field, typeToIdent, warnings);
358
+ if (!arr) return `m.json() /* TODO: unmapped mixed reference */`;
359
+ const refEntries = [];
360
+ if (field.required) refEntries.push("required: true");
361
+ refEntries.push(...filterableEntry(field));
362
+ const opts = optsLiteral(refEntries);
363
+ return opts ? `m.mixedRef(${arr}, ${opts})` : `m.mixedRef(${arr})`;
311
364
  }
312
365
  if (type.startsWith("list.")) {
313
366
  const inner = type.slice("list.".length);
@@ -317,6 +370,7 @@ function fieldCall(field, typeToIdent, warnings) {
317
370
  const max = v(field.validations, "list.max");
318
371
  if (min !== void 0) listEntries.push(`min: ${Number(min)}`);
319
372
  if (max !== void 0) listEntries.push(`max: ${Number(max)}`);
373
+ listEntries.push(...filterableEntry(field));
320
374
  const listOpts = optsLiteral(listEntries);
321
375
  let innerCall;
322
376
  if (inner === "metaobject_reference") {
@@ -327,6 +381,10 @@ function fieldCall(field, typeToIdent, warnings) {
327
381
  return `m.json() /* TODO: unmapped list reference */`;
328
382
  }
329
383
  innerCall = `m.ref(() => ${ident})`;
384
+ } else if (inner === "mixed_reference") {
385
+ const arr = mixedTargetsLiteral(field, typeToIdent, warnings);
386
+ if (!arr) return `m.json() /* TODO: unmapped list mixed reference */`;
387
+ innerCall = `m.mixedRef(${arr})`;
330
388
  } else if (SIMPLE[inner]) {
331
389
  innerCall = scalarCall(SIMPLE[inner], { ...field, required: false }, warnings);
332
390
  } else {
@@ -335,17 +393,53 @@ function fieldCall(field, typeToIdent, warnings) {
335
393
  }
336
394
  return listOpts ? `m.list(${innerCall}, ${listOpts})` : `m.list(${innerCall})`;
337
395
  }
338
- if (SIMPLE[type]) return scalarCall(SIMPLE[type], field, warnings);
396
+ if (SIMPLE[type]) return scalarCall(SIMPLE[type], field, warnings, filterableEntry(field));
339
397
  warnings.push(`unmapped field type "${type}" on field "${field.key}"`);
340
398
  return `m.json() /* TODO: unmapped type ${type} */`;
341
399
  }
400
+ function accessSource(access) {
401
+ if (!access) return "";
402
+ const parts = [];
403
+ if (access.admin === "MERCHANT_READ_WRITE") parts.push(`admin: "merchant_read_write"`);
404
+ if (access.storefront === "PUBLIC_READ") parts.push(`storefront: "public_read"`);
405
+ if (access.customerAccount === "READ") parts.push(`customerAccount: "read"`);
406
+ return parts.length ? `{ ${parts.join(", ")} }` : "";
407
+ }
408
+ function capabilitiesSource(caps) {
409
+ if (!caps) return "";
410
+ const parts = [];
411
+ if (caps.publishable?.enabled) parts.push(`publishable: true`);
412
+ if (caps.translatable?.enabled) parts.push(`translatable: true`);
413
+ if (caps.renderable?.enabled) {
414
+ const d = caps.renderable.data;
415
+ const dparts = [];
416
+ if (d?.metaTitleKey != null) dparts.push(`metaTitleKey: ${JSON.stringify(d.metaTitleKey)}`);
417
+ if (d?.metaDescriptionKey != null) dparts.push(`metaDescriptionKey: ${JSON.stringify(d.metaDescriptionKey)}`);
418
+ parts.push(dparts.length ? `renderable: { ${dparts.join(", ")} }` : `renderable: true`);
419
+ }
420
+ if (caps.onlineStore?.enabled && caps.onlineStore.data?.urlHandle != null) {
421
+ parts.push(`onlineStore: { urlHandle: ${JSON.stringify(caps.onlineStore.data.urlHandle)} }`);
422
+ }
423
+ return parts.length ? `{ ${parts.join(", ")} }` : "";
424
+ }
425
+ function defConfigLines(def) {
426
+ const lines = [];
427
+ if (isMerchant(def.type)) lines.push(` scope: "merchant",`);
428
+ if (def.name) lines.push(` name: ${JSON.stringify(def.name)},`);
429
+ if (def.description != null) lines.push(` description: ${JSON.stringify(def.description)},`);
430
+ if (def.displayNameKey != null) lines.push(` displayName: ${JSON.stringify(def.displayNameKey)},`);
431
+ const access = accessSource(def.access);
432
+ if (access) lines.push(` access: ${access},`);
433
+ const caps = capabilitiesSource(def.capabilities);
434
+ if (caps) lines.push(` capabilities: ${caps},`);
435
+ return lines.length ? `
436
+ ${lines.join("\n")}` : "";
437
+ }
342
438
  function defSource(def, typeToIdent, warnings) {
343
439
  const ident = typeToIdent.get(def.type);
344
440
  const handle = handleOf(def.type);
345
441
  const fields = def.fields.map((f) => ` ${f.key}: ${fieldCall(f, typeToIdent, warnings)},`).join("\n");
346
- const name = def.name ? `
347
- name: ${JSON.stringify(def.name)},` : "";
348
- return `export const ${ident} = defineMetaobject(${JSON.stringify(handle)}, {${name}
442
+ return `export const ${ident} = defineMetaobject(${JSON.stringify(handle)}, {${defConfigLines(def)}
349
443
  fields: {
350
444
  ${fields}
351
445
  },
@@ -357,6 +451,8 @@ function referencedTypes(def) {
357
451
  if (f.type === "metaobject_reference" || f.type === "list.metaobject_reference") {
358
452
  const t = refTarget(f);
359
453
  if (t) out.add(t);
454
+ } else if (f.type === "mixed_reference" || f.type === "list.mixed_reference") {
455
+ for (const t of refTargets(f)) out.add(t);
360
456
  }
361
457
  }
362
458
  return out;
@@ -405,6 +501,42 @@ function sameValidations(a, b) {
405
501
  const norm = (v2) => JSON.stringify([...v2].sort((x, y) => x.name.localeCompare(y.name)));
406
502
  return norm(a) === norm(b);
407
503
  }
504
+ var CAP_KEYS = ["publishable", "translatable", "renderable", "onlineStore"];
505
+ function accessChanged(local, remote) {
506
+ for (const key of ["admin", "storefront", "customerAccount"]) {
507
+ const lv = local[key];
508
+ if (lv != null && remote?.[key] !== lv) return true;
509
+ }
510
+ return false;
511
+ }
512
+ function capabilitiesChanged(local, remote) {
513
+ for (const key of CAP_KEYS) {
514
+ const lc = local[key];
515
+ if (!lc) continue;
516
+ const rc = remote?.[key];
517
+ if ((rc?.enabled ?? false) !== lc.enabled) return true;
518
+ const ldata = lc.data;
519
+ if (ldata) {
520
+ const rdata = rc?.data;
521
+ for (const [k, v2] of Object.entries(ldata)) {
522
+ if (v2 != null && rdata?.[k] !== v2) return true;
523
+ }
524
+ }
525
+ }
526
+ return false;
527
+ }
528
+ function disablesOnlineStore(local, remote) {
529
+ return local.onlineStore?.enabled === false && remote?.onlineStore?.enabled === true;
530
+ }
531
+ function definitionChanges(local, remote) {
532
+ const changes = [];
533
+ if (local.name != null && local.name !== remote.name) changes.push("name");
534
+ if (local.description != null && local.description !== remote.description) changes.push("description");
535
+ if (local.displayNameKey != null && local.displayNameKey !== remote.displayNameKey) changes.push("displayNameKey");
536
+ if (local.access && accessChanged(local.access, remote.access)) changes.push("access");
537
+ if (local.capabilities && capabilitiesChanged(local.capabilities, remote.capabilities)) changes.push("capabilities");
538
+ return changes;
539
+ }
408
540
  function diff(local, remote) {
409
541
  const ops = [];
410
542
  const remoteByType = new Map(remote.map((d) => [d.type, d]));
@@ -428,6 +560,7 @@ function diff(local, remote) {
428
560
  }
429
561
  const changes = {};
430
562
  if (rf.required !== lf.required) changes.required = lf.required;
563
+ if (rf.filterable !== lf.filterable) changes.filterable = lf.filterable;
431
564
  if (!sameValidations(rf.validations, lf.validations)) changes.validations = lf.validations;
432
565
  if (Object.keys(changes).length) {
433
566
  ops.push({ kind: "updateField", type: localDef.type, key: lf.key, changes });
@@ -438,49 +571,176 @@ function diff(local, remote) {
438
571
  ops.push({ kind: "removeField", type: localDef.type, key: rf.key, destructive: true });
439
572
  }
440
573
  }
574
+ const metaChanges = definitionChanges(localDef, remoteDef);
575
+ if (metaChanges.length) {
576
+ const destructive = localDef.capabilities ? disablesOnlineStore(localDef.capabilities, remoteDef.capabilities) : false;
577
+ ops.push({
578
+ kind: "updateDefinition",
579
+ type: localDef.type,
580
+ changes: metaChanges,
581
+ ...destructive ? { destructive: true } : {}
582
+ });
583
+ }
441
584
  }
442
585
  return ops;
443
586
  }
444
587
 
445
588
  // src/sync/normalize.ts
446
- function normalizeLocal(schema) {
447
- const def = schema.toDefinitionInput();
448
- return {
589
+ function localCapabilities(caps) {
590
+ const out = {};
591
+ if (caps?.publishable) out.publishable = { enabled: caps.publishable.enabled };
592
+ if (caps?.translatable) out.translatable = { enabled: caps.translatable.enabled };
593
+ if (caps?.renderable) {
594
+ out.renderable = { enabled: caps.renderable.enabled };
595
+ if (caps.renderable.data) out.renderable.data = { ...caps.renderable.data };
596
+ }
597
+ out.onlineStore = caps?.onlineStore ? { enabled: caps.onlineStore.enabled, data: { urlHandle: caps.onlineStore.data?.urlHandle } } : { enabled: false };
598
+ return out;
599
+ }
600
+ function normalizeDefinition(def) {
601
+ const out = {
449
602
  type: def.type,
450
603
  name: def.name,
604
+ capabilities: localCapabilities(def.capabilities),
451
605
  fields: def.fieldDefinitions.map((f) => ({
452
606
  key: f.key,
453
607
  type: f.type,
454
608
  required: f.required,
609
+ filterable: f.capabilities?.adminFilterable?.enabled ?? false,
455
610
  validations: f.validations
456
611
  }))
457
612
  };
613
+ if (def.description != null) out.description = def.description;
614
+ if (def.displayNameKey != null) out.displayNameKey = def.displayNameKey;
615
+ if (def.access) out.access = { ...def.access };
616
+ return out;
617
+ }
618
+ function remoteAccess(access) {
619
+ if (!access) return void 0;
620
+ const out = {};
621
+ if (access.admin != null) out.admin = access.admin;
622
+ if (access.storefront != null) out.storefront = access.storefront;
623
+ if (access.customerAccount != null) out.customerAccount = access.customerAccount;
624
+ return out;
625
+ }
626
+ function remoteCapabilities(caps) {
627
+ const out = {};
628
+ if (!caps) return out;
629
+ if (caps.publishable) out.publishable = { enabled: !!caps.publishable.enabled };
630
+ if (caps.translatable) out.translatable = { enabled: !!caps.translatable.enabled };
631
+ if (caps.renderable) {
632
+ out.renderable = { enabled: !!caps.renderable.enabled };
633
+ const d = caps.renderable.data;
634
+ if (d) {
635
+ const data = {};
636
+ if (d.metaTitleKey != null) data.metaTitleKey = d.metaTitleKey;
637
+ if (d.metaDescriptionKey != null) data.metaDescriptionKey = d.metaDescriptionKey;
638
+ if (Object.keys(data).length) out.renderable.data = data;
639
+ }
640
+ }
641
+ if (caps.onlineStore) {
642
+ out.onlineStore = { enabled: !!caps.onlineStore.enabled };
643
+ if (caps.onlineStore.data?.urlHandle != null) out.onlineStore.data = { urlHandle: caps.onlineStore.data.urlHandle };
644
+ }
645
+ return out;
458
646
  }
459
647
  function normalizeRemote(def) {
460
- return {
648
+ const out = {
461
649
  type: def.type,
462
650
  name: def.name,
651
+ capabilities: remoteCapabilities(def.capabilities),
463
652
  fields: def.fieldDefinitions.map((f) => ({
464
653
  key: f.key,
465
654
  type: typeof f.type === "string" ? f.type : f.type.name,
466
655
  required: f.required,
656
+ filterable: f.capabilities?.adminFilterable?.enabled ?? false,
467
657
  validations: f.validations ?? []
468
658
  }))
469
659
  };
660
+ if (def.description != null) out.description = def.description;
661
+ if (def.displayNameKey != null) out.displayNameKey = def.displayNameKey;
662
+ const access = remoteAccess(def.access);
663
+ if (access) out.access = access;
664
+ return out;
665
+ }
666
+
667
+ // src/sync/resolve.ts
668
+ var APP_PREFIX2 = "$app:";
669
+ function effectiveScope(schema, config) {
670
+ return schema.config.scope ?? config.scope ?? "app";
671
+ }
672
+ function effectiveType(handle, scope) {
673
+ return scope === "merchant" ? handle : `${APP_PREFIX2}${handle}`;
674
+ }
675
+ function rewriteReference(v2, effByCanonical) {
676
+ if (v2.name === "metaobject_definition_type") {
677
+ const mapped = effByCanonical.get(v2.value);
678
+ return mapped ? { ...v2, value: mapped } : v2;
679
+ }
680
+ if (v2.name === "metaobject_definition_types") {
681
+ try {
682
+ const parsed = JSON.parse(v2.value);
683
+ if (Array.isArray(parsed)) {
684
+ const rewritten = parsed.map((t) => typeof t === "string" ? effByCanonical.get(t) ?? t : t);
685
+ return { ...v2, value: JSON.stringify(rewritten) };
686
+ }
687
+ } catch {
688
+ }
689
+ }
690
+ return v2;
691
+ }
692
+ function resolveAdmin(out, handle, scope, config) {
693
+ const explicitAdmin = out.access?.admin;
694
+ if (scope === "merchant") {
695
+ if (explicitAdmin != null) {
696
+ throw new Error(
697
+ `"${handle}" is merchant-scoped but sets access.admin; admin access is only valid on app-scoped metaobjects.`
698
+ );
699
+ }
700
+ return;
701
+ }
702
+ if (explicitAdmin == null) {
703
+ const admin = config.merchantEditable ? "MERCHANT_READ_WRITE" : "MERCHANT_READ";
704
+ out.access = { ...out.access, admin };
705
+ }
706
+ }
707
+ function resolveDefinitions(schemas, config = {}) {
708
+ const effByCanonical = /* @__PURE__ */ new Map();
709
+ for (const s of schemas) {
710
+ effByCanonical.set(`${APP_PREFIX2}${s.handle}`, effectiveType(s.handle, effectiveScope(s, config)));
711
+ }
712
+ return schemas.map((s) => {
713
+ const scope = effectiveScope(s, config);
714
+ const base = s.toDefinitionInput();
715
+ const fieldDefinitions = base.fieldDefinitions.map((f) => ({
716
+ ...f,
717
+ validations: f.validations.map((v2) => rewriteReference(v2, effByCanonical))
718
+ }));
719
+ const out = { ...base, type: effectiveType(s.handle, scope), fieldDefinitions };
720
+ resolveAdmin(out, s.handle, scope, config);
721
+ return out;
722
+ });
470
723
  }
471
724
 
472
725
  // src/sync/pull.ts
726
+ function toDefinition(type, node) {
727
+ return {
728
+ type,
729
+ name: node.name,
730
+ description: node.description,
731
+ displayNameKey: node.displayNameKey,
732
+ access: node.access,
733
+ capabilities: node.capabilities,
734
+ fieldDefinitions: node.fieldDefinitions
735
+ };
736
+ }
473
737
  async function pull(client, types) {
474
738
  const out = [];
475
739
  for (const type of types) {
476
740
  const data = await execute(client, PULL_DEFINITION_QUERY, { type });
477
741
  const node = data.metaobjectDefinitionByType;
478
742
  if (!node) continue;
479
- out.push({
480
- id: node.id,
481
- type,
482
- definition: { type, name: node.name, fieldDefinitions: node.fieldDefinitions }
483
- });
743
+ out.push({ id: node.id, type, definition: toDefinition(type, node) });
484
744
  }
485
745
  return out;
486
746
  }
@@ -498,7 +758,7 @@ async function pullAll(client, opts = {}) {
498
758
  const canonical = toCanonicalType(node.type);
499
759
  if (appOwnedOnly && !canonical) continue;
500
760
  const type = canonical ?? node.type;
501
- out.push({ id: node.id, type, definition: { type, name: node.name, fieldDefinitions: node.fieldDefinitions } });
761
+ out.push({ id: node.id, type, definition: toDefinition(type, node) });
502
762
  }
503
763
  after = data.metaobjectDefinitions.pageInfo.hasNextPage ? data.metaobjectDefinitions.pageInfo.endCursor : null;
504
764
  } while (after !== null);
@@ -506,28 +766,57 @@ async function pullAll(client, opts = {}) {
506
766
  }
507
767
 
508
768
  // src/sync/push.ts
769
+ function definitionUpdateFor(def, changes) {
770
+ const out = {};
771
+ for (const c of changes) {
772
+ if (c === "name") out.name = def.name;
773
+ else if (c === "description") out.description = def.description;
774
+ else if (c === "displayNameKey") out.displayNameKey = def.displayNameKey;
775
+ else if (c === "access") out.access = def.access;
776
+ else if (c === "capabilities") {
777
+ const caps = def.capabilities;
778
+ const payload = {};
779
+ if (caps?.publishable) payload.publishable = caps.publishable;
780
+ if (caps?.translatable) payload.translatable = caps.translatable;
781
+ if (caps?.renderable) payload.renderable = caps.renderable;
782
+ payload.onlineStore = caps?.onlineStore ?? { enabled: false };
783
+ out.capabilities = payload;
784
+ }
785
+ }
786
+ return out;
787
+ }
509
788
  function fieldInputFor(defByType, type, key) {
510
789
  return defByType.get(type)?.fieldDefinitions.find((f) => f.key === key);
511
790
  }
512
- function referenceEdges(def) {
791
+ function fieldRefTargets(field) {
513
792
  const out = [];
514
- for (const field of def.fieldDefinitions) {
515
- for (const v2 of field.validations) {
516
- if (v2.name === "metaobject_definition_type") {
517
- out.push(v2.value);
518
- } else if (v2.name === "metaobject_definition_types") {
519
- try {
520
- const parsed = JSON.parse(v2.value);
521
- if (Array.isArray(parsed)) {
522
- for (const t of parsed) if (typeof t === "string") out.push(t);
523
- }
524
- } catch {
793
+ for (const v2 of field.validations) {
794
+ if (v2.name === "metaobject_definition_type") {
795
+ out.push(v2.value);
796
+ } else if (v2.name === "metaobject_definition_types") {
797
+ try {
798
+ const parsed = JSON.parse(v2.value);
799
+ if (Array.isArray(parsed)) {
800
+ for (const t of parsed) if (typeof t === "string") out.push(t);
525
801
  }
802
+ } catch {
526
803
  }
527
804
  }
528
805
  }
529
806
  return out;
530
807
  }
808
+ function referenceEdges(def) {
809
+ return def.fieldDefinitions.flatMap(fieldRefTargets);
810
+ }
811
+ function splitCyclicFields(def, cyclicTypes) {
812
+ const pass1 = [];
813
+ const deferred = [];
814
+ for (const f of def.fieldDefinitions) {
815
+ const breaksCycle = fieldRefTargets(f).some((t) => cyclicTypes.has(t) && t !== def.type);
816
+ (breaksCycle ? deferred : pass1).push(f);
817
+ }
818
+ return { pass1, deferred };
819
+ }
531
820
  function topoSortCreates(types, deps) {
532
821
  const remaining = /* @__PURE__ */ new Map();
533
822
  const dependents = /* @__PURE__ */ new Map();
@@ -570,50 +859,51 @@ async function push(client, plan, sources, options) {
570
859
  deps.set(op.type, new Set(targets.filter((t) => createTypes.has(t) && t !== op.type)));
571
860
  }
572
861
  const { ordered, unordered } = topoSortCreates(createTypes, deps);
573
- const orderedSet = new Set(ordered);
574
862
  const cyclicTypes = new Set(unordered);
575
863
  const createByType = new Map(createOps.map((x) => [x.op.type, x]));
576
- const execOrder = [
577
- ...ordered.map((t) => createByType.get(t)),
578
- ...createOps.filter((x) => !orderedSet.has(x.op.type)),
579
- ...otherOps
580
- ];
581
864
  const failedTypes = /* @__PURE__ */ new Set();
582
- async function applyOp(op) {
583
- if (op.kind === "createDefinition") {
584
- if (cyclicTypes.has(op.type)) {
585
- failedTypes.add(op.type);
586
- return { op, status: "blocked", reason: "reference cycle \u2014 two-pass create deferred" };
587
- }
588
- for (const dep of deps.get(op.type) ?? []) {
589
- if (failedTypes.has(dep)) {
590
- failedTypes.add(op.type);
591
- return { op, status: "blocked", reason: `blocked: dependency "${dep}" was not created` };
592
- }
593
- }
594
- const def = defByType.get(op.type);
595
- if (!def) {
596
- failedTypes.add(op.type);
597
- return { op, status: "blocked", reason: `no definition input for "${op.type}"` };
598
- }
599
- const data2 = await execute(client, CREATE_DEFINITION_MUTATION, { definition: def });
600
- const payload2 = data2.metaobjectDefinitionCreate;
601
- if (payload2.userErrors.length) {
865
+ async function createDefinition(op, definition) {
866
+ const data = await execute(client, CREATE_DEFINITION_MUTATION, { definition });
867
+ const payload = data.metaobjectDefinitionCreate;
868
+ if (payload.userErrors.length) {
869
+ failedTypes.add(op.type);
870
+ return { op, status: "failed", userErrors: payload.userErrors };
871
+ }
872
+ const id = payload.metaobjectDefinition?.id;
873
+ if (id) idByType.set(op.type, id);
874
+ return { op, status: "applied", id };
875
+ }
876
+ async function applyAcyclicCreate(op) {
877
+ for (const dep of deps.get(op.type) ?? []) {
878
+ if (failedTypes.has(dep)) {
602
879
  failedTypes.add(op.type);
603
- return { op, status: "failed", userErrors: payload2.userErrors };
880
+ return { op, status: "blocked", reason: `blocked: dependency "${dep}" was not created` };
604
881
  }
605
- const id2 = payload2.metaobjectDefinition?.id;
606
- if (id2) idByType.set(op.type, id2);
607
- return { op, status: "applied", id: id2 };
608
882
  }
609
- const destructive = op.kind === "removeField" || op.kind === "changeFieldType";
883
+ const def = defByType.get(op.type);
884
+ if (!def) {
885
+ failedTypes.add(op.type);
886
+ return { op, status: "blocked", reason: `no definition input for "${op.type}"` };
887
+ }
888
+ return createDefinition(op, def);
889
+ }
890
+ async function applyFieldOp(op) {
891
+ const destructive = "destructive" in op && op.destructive === true;
610
892
  if (destructive && !allowDestructive) return { op, status: "skipped", reason: "destructive" };
611
893
  if (failedTypes.has(op.type)) return { op, status: "blocked", reason: `blocked: definition "${op.type}" was not created` };
612
894
  const id = idByType.get(op.type);
613
895
  if (id == null) return { op, status: "blocked", reason: `no definition id for "${op.type}"` };
614
- const fieldDefinitions = fieldOpsFor(op);
615
- if (!fieldDefinitions) return { op, status: "blocked", reason: `no field input for "${op.type}"` };
616
- const data = await execute(client, UPDATE_DEFINITION_MUTATION, { id, definition: { fieldDefinitions } });
896
+ let definition;
897
+ if (op.kind === "updateDefinition") {
898
+ const def = defByType.get(op.type);
899
+ if (!def) return { op, status: "blocked", reason: `no definition input for "${op.type}"` };
900
+ definition = definitionUpdateFor(def, op.changes);
901
+ } else {
902
+ const fieldDefinitions = fieldOpsFor(op);
903
+ if (!fieldDefinitions) return { op, status: "blocked", reason: `no field input for "${op.type}"` };
904
+ definition = { fieldDefinitions };
905
+ }
906
+ const data = await execute(client, UPDATE_DEFINITION_MUTATION, { id, definition });
617
907
  const payload = data.metaobjectDefinitionUpdate;
618
908
  if (payload.userErrors.length) return { op, status: "failed", userErrors: payload.userErrors };
619
909
  return { op, status: "applied", id: payload.metaobjectDefinition?.id ?? id };
@@ -634,6 +924,9 @@ async function push(client, plan, sources, options) {
634
924
  validations: field.validations
635
925
  };
636
926
  if (field.description != null) update.description = field.description;
927
+ if ("filterable" in op.changes) {
928
+ update.capabilities = { adminFilterable: { enabled: op.changes.filterable ?? false } };
929
+ }
637
930
  return [{ update }];
638
931
  }
639
932
  case "removeField":
@@ -647,19 +940,84 @@ async function push(client, plan, sources, options) {
647
940
  }
648
941
  }
649
942
  const results = new Array(plan.length);
650
- for (const { op, index } of execOrder) results[index] = await applyOp(op);
943
+ for (const type of ordered) {
944
+ const entry = createByType.get(type);
945
+ results[entry.index] = await applyAcyclicCreate(entry.op);
946
+ }
947
+ const cyclicCreates = createOps.filter((x) => cyclicTypes.has(x.op.type));
948
+ const deferredByType = /* @__PURE__ */ new Map();
949
+ for (const { op, index } of cyclicCreates) {
950
+ const def = defByType.get(op.type);
951
+ if (!def) {
952
+ failedTypes.add(op.type);
953
+ results[index] = { op, status: "blocked", reason: `no definition input for "${op.type}"` };
954
+ continue;
955
+ }
956
+ const failedDep = [...deps.get(op.type) ?? []].find((d) => !cyclicTypes.has(d) && failedTypes.has(d));
957
+ if (failedDep) {
958
+ failedTypes.add(op.type);
959
+ results[index] = { op, status: "blocked", reason: `blocked: dependency "${failedDep}" was not created` };
960
+ continue;
961
+ }
962
+ const { pass1, deferred } = splitCyclicFields(def, cyclicTypes);
963
+ deferredByType.set(op.type, deferred);
964
+ results[index] = await createDefinition(op, { ...def, fieldDefinitions: pass1 });
965
+ }
966
+ for (const { op, index } of cyclicCreates) {
967
+ if (results[index]?.status !== "applied") continue;
968
+ const deferred = deferredByType.get(op.type) ?? [];
969
+ if (!deferred.length) continue;
970
+ const failedTarget = deferred.flatMap(fieldRefTargets).find((t) => failedTypes.has(t));
971
+ if (failedTarget) {
972
+ failedTypes.add(op.type);
973
+ results[index] = { op, status: "blocked", reason: `blocked: dependency "${failedTarget}" was not created` };
974
+ continue;
975
+ }
976
+ const id = idByType.get(op.type);
977
+ if (id == null) {
978
+ results[index] = { op, status: "blocked", reason: `no definition id for "${op.type}"` };
979
+ continue;
980
+ }
981
+ const definition = { fieldDefinitions: deferred.map((f) => ({ create: f })) };
982
+ const data = await execute(client, UPDATE_DEFINITION_MUTATION, { id, definition });
983
+ const payload = data.metaobjectDefinitionUpdate;
984
+ if (payload.userErrors.length) {
985
+ failedTypes.add(op.type);
986
+ results[index] = { op, status: "failed", userErrors: payload.userErrors };
987
+ }
988
+ }
989
+ for (const { op, index } of otherOps) results[index] = await applyFieldOp(op);
651
990
  const counts = { applied: 0, skipped: 0, blocked: 0, failed: 0 };
652
991
  for (const r of results) counts[r.status]++;
653
992
  return { results, counts, ok: counts.failed === 0 && counts.blocked === 0 };
654
993
  }
655
994
 
656
995
  // src/cli/plan.ts
657
- async function planFor(client, schemas) {
658
- const types = schemas.map((s) => s.type);
659
- const localDefs = schemas.map(normalizeLocal);
996
+ var APP_PREFIX3 = "$app:";
997
+ async function detectScopeFlips(client, definitions, plan) {
998
+ const merchant = definitions.filter((d) => !d.type.startsWith(APP_PREFIX3));
999
+ if (merchant.length === 0) return [];
1000
+ const created = new Set(plan.filter((op) => op.kind === "createDefinition").map((op) => op.type));
1001
+ const shadows = await pull(client, merchant.map((d) => `${APP_PREFIX3}${d.type}`));
1002
+ const shadowTypes = new Set(shadows.map((s) => s.type));
1003
+ const warnings = [];
1004
+ for (const d of merchant) {
1005
+ if (created.has(d.type) && shadowTypes.has(`${APP_PREFIX3}${d.type}`)) {
1006
+ warnings.push(
1007
+ `"${d.type}" is merchant-scoped but an app-owned "${APP_PREFIX3}${d.type}" already exists remotely. Scope changes are not migrated (type is immutable): a new merchant-owned definition will be created and the app-owned one left orphaned. Migrate manually \u2014 see docs/SYNC.md.`
1008
+ );
1009
+ }
1010
+ }
1011
+ return warnings;
1012
+ }
1013
+ async function planFor(client, schemas, config = {}) {
1014
+ const definitions = resolveDefinitions(schemas, config);
1015
+ const types = definitions.map((d) => d.type);
1016
+ const localDefs = definitions.map(normalizeDefinition);
660
1017
  const remote = await pull(client, types);
661
1018
  const plan = diff(localDefs, remote.map((r) => normalizeRemote(r.definition)));
662
- return { plan, remote };
1019
+ const warnings = await detectScopeFlips(client, definitions, plan);
1020
+ return { plan, remote, definitions, warnings };
663
1021
  }
664
1022
 
665
1023
  // src/cli/format.ts
@@ -690,7 +1048,8 @@ function describeResult(r) {
690
1048
 
691
1049
  // src/cli/diff.ts
692
1050
  async function runDiff(args) {
693
- const { plan } = await planFor(args.client, args.schemas);
1051
+ const { plan, warnings } = await planFor(args.client, args.schemas, args.config);
1052
+ for (const w of warnings) console.warn(`Warning: ${w}`);
694
1053
  if (plan.length === 0) {
695
1054
  console.log("Everything is in sync \u2014 nothing to apply.");
696
1055
  } else {
@@ -702,8 +1061,8 @@ async function runDiff(args) {
702
1061
 
703
1062
  // src/cli/push.ts
704
1063
  async function runPush(args) {
705
- const { plan, remote } = await planFor(args.client, args.schemas);
706
- const definitions = args.schemas.map((s) => s.toDefinitionInput());
1064
+ const { plan, remote, definitions, warnings } = await planFor(args.client, args.schemas, args.config);
1065
+ for (const w of warnings) console.warn(`Warning: ${w}`);
707
1066
  const result = await push(args.client, plan, { definitions, remote }, { allowDestructive: args.allowDestructive });
708
1067
  for (const r of result.results) console.log(` ${describeResult(r)}`);
709
1068
  console.log(
@@ -789,11 +1148,11 @@ async function main(argv) {
789
1148
  }
790
1149
  const schemas = await loadSchemas(config.schema);
791
1150
  if (args.command === "diff") {
792
- await runDiff({ client, schemas });
1151
+ await runDiff({ client, schemas, config });
793
1152
  return 0;
794
1153
  }
795
1154
  if (args.command === "push") {
796
- const result = await runPush({ client, schemas, allowDestructive: args.allowDestructive });
1155
+ const result = await runPush({ client, schemas, config, allowDestructive: args.allowDestructive });
797
1156
  return result.ok ? 0 : 2;
798
1157
  }
799
1158
  console.error(`Unknown command: ${args.command}`);