@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.
package/dist/index.cjs CHANGED
@@ -28,11 +28,13 @@ __export(src_exports, {
28
28
  diff: () => diff,
29
29
  generateSchemaSource: () => generateSchemaSource,
30
30
  m: () => m,
31
+ normalizeDefinition: () => normalizeDefinition,
31
32
  normalizeLocal: () => normalizeLocal,
32
33
  normalizeRemote: () => normalizeRemote,
33
34
  pull: () => pull,
34
35
  pullAll: () => pullAll,
35
36
  push: () => push,
37
+ resolveDefinitions: () => resolveDefinitions,
36
38
  toDefinitionInput: () => toDefinitionInput,
37
39
  validateConfig: () => validateConfig
38
40
  });
@@ -60,6 +62,14 @@ var Field = class {
60
62
  required = false;
61
63
  name;
62
64
  description;
65
+ filterable = false;
66
+ /** Apply the shared field options; call from each concrete constructor. */
67
+ applyCommon(opts) {
68
+ this.required = opts.required ?? false;
69
+ this.name = opts.name;
70
+ this.description = opts.description;
71
+ this.filterable = opts.filterable ?? false;
72
+ }
63
73
  /** Constraint validation on an already-typed value. */
64
74
  check(_value) {
65
75
  return [];
@@ -116,9 +126,7 @@ var BooleanField = class extends Field {
116
126
  shopifyType = "boolean";
117
127
  constructor(opts) {
118
128
  super();
119
- this.required = opts.required ?? false;
120
- this.name = opts.name;
121
- this.description = opts.description;
129
+ this.applyCommon(opts);
122
130
  }
123
131
  validations() {
124
132
  return [];
@@ -144,9 +152,7 @@ var ListField = class extends Field {
144
152
  this.inner = inner;
145
153
  this.o = o;
146
154
  this.shopifyType = `list.${inner.shopifyType}`;
147
- this.required = o.required ?? false;
148
- this.name = o.name;
149
- this.description = o.description;
155
+ this.applyCommon(o);
150
156
  }
151
157
  inner;
152
158
  o;
@@ -182,9 +188,7 @@ var MeasurementField = class extends Field {
182
188
  constructor(opts, shopifyType) {
183
189
  super();
184
190
  this.shopifyType = shopifyType;
185
- this.required = opts.required ?? false;
186
- this.name = opts.name;
187
- this.description = opts.description;
191
+ this.applyCommon(opts);
188
192
  }
189
193
  shopifyType;
190
194
  wireIsJson = true;
@@ -220,9 +224,7 @@ var MoneyField = class extends Field {
220
224
  wireIsJson = true;
221
225
  constructor(opts) {
222
226
  super();
223
- this.required = opts.required ?? false;
224
- this.name = opts.name;
225
- this.description = opts.description;
227
+ this.applyCommon(opts);
226
228
  }
227
229
  validations() {
228
230
  return [];
@@ -248,9 +250,7 @@ var NumericField = class extends Field {
248
250
  constructor(opts) {
249
251
  super();
250
252
  this.opts = opts;
251
- this.required = opts.required ?? false;
252
- this.name = opts.name;
253
- this.description = opts.description;
253
+ this.applyCommon(opts);
254
254
  }
255
255
  opts;
256
256
  toJson(value) {
@@ -315,9 +315,7 @@ var RatingField = class extends Field {
315
315
  constructor(o) {
316
316
  super();
317
317
  this.o = o;
318
- this.required = o.required ?? false;
319
- this.name = o.name;
320
- this.description = o.description;
318
+ this.applyCommon(o);
321
319
  }
322
320
  o;
323
321
  shopifyType = "rating";
@@ -372,9 +370,7 @@ function rating(opts) {
372
370
  var GidField = class extends Field {
373
371
  constructor(opts) {
374
372
  super();
375
- this.required = opts.required ?? false;
376
- this.name = opts.name;
377
- this.description = opts.description;
373
+ this.applyCommon(opts);
378
374
  }
379
375
  toJson(value) {
380
376
  return value;
@@ -419,6 +415,17 @@ var MetaobjectRefField = class extends GidField {
419
415
  return [{ name: "metaobject_definition_type", value: resolveType(this.target) }];
420
416
  }
421
417
  };
418
+ var MixedRefField = class extends GidField {
419
+ constructor(targets, opts) {
420
+ super(opts);
421
+ this.targets = targets;
422
+ }
423
+ targets;
424
+ shopifyType = "mixed_reference";
425
+ validations() {
426
+ return [{ name: "metaobject_definition_types", value: JSON.stringify(this.targets.map(resolveType)) }];
427
+ }
428
+ };
422
429
  function product(opts = {}) {
423
430
  return new SimpleRefField(opts, "product_reference");
424
431
  }
@@ -437,14 +444,15 @@ function file(opts = {}) {
437
444
  function ref(target, opts = {}) {
438
445
  return new MetaobjectRefField(target, opts);
439
446
  }
447
+ function mixedRef(targets, opts = {}) {
448
+ return new MixedRefField(targets, opts);
449
+ }
440
450
 
441
451
  // src/fields/scalar.ts
442
452
  var StringScalarField = class extends Field {
443
453
  constructor(opts) {
444
454
  super();
445
- this.required = opts.required ?? false;
446
- this.name = opts.name;
447
- this.description = opts.description;
455
+ this.applyCommon(opts);
448
456
  }
449
457
  toJson(value) {
450
458
  return value;
@@ -494,9 +502,7 @@ var JsonField = class extends Field {
494
502
  wireIsJson = true;
495
503
  constructor(opts) {
496
504
  super();
497
- this.required = opts.required ?? false;
498
- this.name = opts.name;
499
- this.description = opts.description;
505
+ this.applyCommon(opts);
500
506
  }
501
507
  validations() {
502
508
  return [];
@@ -530,9 +536,7 @@ var TextField = class extends Field {
530
536
  super();
531
537
  this.opts = opts;
532
538
  this.shopifyType = multiline ? "multi_line_text_field" : "single_line_text_field";
533
- this.required = opts.required ?? false;
534
- this.name = opts.name;
535
- this.description = opts.description;
539
+ this.applyCommon(opts);
536
540
  }
537
541
  opts;
538
542
  shopifyType;
@@ -591,6 +595,7 @@ var m = {
591
595
  page,
592
596
  file,
593
597
  ref,
598
+ mixedRef,
594
599
  list
595
600
  };
596
601
 
@@ -600,16 +605,44 @@ function mapAccess(access) {
600
605
  const out = {};
601
606
  if (access.admin) out.admin = access.admin.toUpperCase();
602
607
  if (access.storefront) out.storefront = access.storefront.toUpperCase();
608
+ if (access.customerAccount) out.customerAccount = access.customerAccount.toUpperCase();
603
609
  return out;
604
610
  }
605
611
  function mapCapabilities(caps) {
606
612
  if (!caps) return void 0;
607
613
  const out = {};
608
- for (const [k, v2] of Object.entries(caps)) if (v2 != null) out[k] = { enabled: v2 };
614
+ if (caps.publishable != null) out.publishable = { enabled: caps.publishable };
615
+ if (caps.translatable != null) out.translatable = { enabled: caps.translatable };
616
+ if (caps.renderable != null) {
617
+ if (typeof caps.renderable === "boolean") {
618
+ out.renderable = { enabled: caps.renderable };
619
+ } else {
620
+ const data = {};
621
+ if (caps.renderable.metaTitleKey != null) data.metaTitleKey = caps.renderable.metaTitleKey;
622
+ if (caps.renderable.metaDescriptionKey != null) data.metaDescriptionKey = caps.renderable.metaDescriptionKey;
623
+ out.renderable = Object.keys(data).length ? { enabled: true, data } : { enabled: true };
624
+ }
625
+ }
626
+ if (caps.onlineStore != null) {
627
+ const data = { urlHandle: caps.onlineStore.urlHandle };
628
+ if (caps.onlineStore.createRedirects != null) data.createRedirects = caps.onlineStore.createRedirects;
629
+ out.onlineStore = { enabled: true, data };
630
+ }
609
631
  return Object.keys(out).length ? out : void 0;
610
632
  }
633
+ function validateSeoKeys(caps, fieldKeys, name) {
634
+ const r = caps?.renderable;
635
+ if (!r || typeof r === "boolean") return;
636
+ for (const prop of ["metaTitleKey", "metaDescriptionKey"]) {
637
+ const key = r[prop];
638
+ if (key != null && !fieldKeys.has(key)) {
639
+ throw new Error(`renderable.${prop} "${key}" is not a declared field key on "${name}".`);
640
+ }
641
+ }
642
+ }
611
643
  function toDefinitionInput(schema) {
612
644
  const { config } = schema;
645
+ validateSeoKeys(config.capabilities, new Set(Object.keys(schema.fields)), config.name);
613
646
  const fieldDefinitions = Object.entries(schema.fields).map(([key, field]) => {
614
647
  const def = {
615
648
  key,
@@ -619,6 +652,7 @@ function toDefinitionInput(schema) {
619
652
  validations: field.validations()
620
653
  };
621
654
  if (field.description != null) def.description = field.description;
655
+ if (field.filterable) def.capabilities = { adminFilterable: { enabled: true } };
622
656
  return def;
623
657
  });
624
658
  const out = {
@@ -691,6 +725,12 @@ function defineMetaobject(handle, config) {
691
725
 
692
726
  // src/codegen.ts
693
727
  var APP_PREFIX = "$app:";
728
+ function isMerchant(type) {
729
+ return !type.startsWith(APP_PREFIX);
730
+ }
731
+ function filterableEntry(field) {
732
+ return field.filterable ? ["filterable: true"] : [];
733
+ }
694
734
  var SIMPLE = {
695
735
  single_line_text_field: "text",
696
736
  multi_line_text_field: "multilineText",
@@ -734,6 +774,27 @@ function refTarget(field) {
734
774
  }
735
775
  return void 0;
736
776
  }
777
+ function refTargets(field) {
778
+ const many = v(field.validations, "metaobject_definition_types");
779
+ if (many) {
780
+ try {
781
+ const arr = JSON.parse(many);
782
+ if (Array.isArray(arr)) return arr.map(String);
783
+ } catch {
784
+ }
785
+ }
786
+ const single = v(field.validations, "metaobject_definition_type");
787
+ return single ? [single] : [];
788
+ }
789
+ function mixedTargetsLiteral(field, typeToIdent, warnings) {
790
+ const targets = refTargets(field);
791
+ const idents = targets.map((t) => typeToIdent.get(t));
792
+ if (!targets.length || idents.some((i) => !i)) {
793
+ warnings.push(`unresolved mixed reference on field "${field.key}"`);
794
+ return void 0;
795
+ }
796
+ return `[${idents.map((i) => `() => ${i}`).join(", ")}]`;
797
+ }
737
798
  function optsLiteral(entries) {
738
799
  return entries.length ? `{ ${entries.join(", ")} }` : "";
739
800
  }
@@ -772,8 +833,8 @@ function scalarEntries(field, warnings, builder) {
772
833
  jsonArr("file_type_options", "accept");
773
834
  return e;
774
835
  }
775
- function scalarCall(builder, field, warnings) {
776
- const lit = optsLiteral(scalarEntries(field, warnings, builder));
836
+ function scalarCall(builder, field, warnings, extra = []) {
837
+ const lit = optsLiteral([...scalarEntries(field, warnings, builder), ...extra]);
777
838
  return lit ? `m.${builder}(${lit})` : `m.${builder}()`;
778
839
  }
779
840
  function fieldCall(field, typeToIdent, warnings) {
@@ -783,6 +844,7 @@ function fieldCall(field, typeToIdent, warnings) {
783
844
  const max = v(field.validations, "max");
784
845
  const e = [`min: ${Number(min ?? 1)}`, `max: ${Number(max ?? 5)}`];
785
846
  if (field.required) e.unshift("required: true");
847
+ e.push(...filterableEntry(field));
786
848
  if (min === void 0 || max === void 0) warnings.push(`rating field "${field.key}" missing min/max`);
787
849
  return `m.rating(${optsLiteral(e)})`;
788
850
  }
@@ -793,7 +855,20 @@ function fieldCall(field, typeToIdent, warnings) {
793
855
  warnings.push(`unresolved reference on field "${field.key}"`);
794
856
  return `m.json() /* TODO: unmapped reference */`;
795
857
  }
796
- return field.required ? `m.ref(() => ${ident}, { required: true })` : `m.ref(() => ${ident})`;
858
+ const refEntries = [];
859
+ if (field.required) refEntries.push("required: true");
860
+ refEntries.push(...filterableEntry(field));
861
+ const opts = optsLiteral(refEntries);
862
+ return opts ? `m.ref(() => ${ident}, ${opts})` : `m.ref(() => ${ident})`;
863
+ }
864
+ if (type === "mixed_reference") {
865
+ const arr = mixedTargetsLiteral(field, typeToIdent, warnings);
866
+ if (!arr) return `m.json() /* TODO: unmapped mixed reference */`;
867
+ const refEntries = [];
868
+ if (field.required) refEntries.push("required: true");
869
+ refEntries.push(...filterableEntry(field));
870
+ const opts = optsLiteral(refEntries);
871
+ return opts ? `m.mixedRef(${arr}, ${opts})` : `m.mixedRef(${arr})`;
797
872
  }
798
873
  if (type.startsWith("list.")) {
799
874
  const inner = type.slice("list.".length);
@@ -803,6 +878,7 @@ function fieldCall(field, typeToIdent, warnings) {
803
878
  const max = v(field.validations, "list.max");
804
879
  if (min !== void 0) listEntries.push(`min: ${Number(min)}`);
805
880
  if (max !== void 0) listEntries.push(`max: ${Number(max)}`);
881
+ listEntries.push(...filterableEntry(field));
806
882
  const listOpts = optsLiteral(listEntries);
807
883
  let innerCall;
808
884
  if (inner === "metaobject_reference") {
@@ -813,6 +889,10 @@ function fieldCall(field, typeToIdent, warnings) {
813
889
  return `m.json() /* TODO: unmapped list reference */`;
814
890
  }
815
891
  innerCall = `m.ref(() => ${ident})`;
892
+ } else if (inner === "mixed_reference") {
893
+ const arr = mixedTargetsLiteral(field, typeToIdent, warnings);
894
+ if (!arr) return `m.json() /* TODO: unmapped list mixed reference */`;
895
+ innerCall = `m.mixedRef(${arr})`;
816
896
  } else if (SIMPLE[inner]) {
817
897
  innerCall = scalarCall(SIMPLE[inner], { ...field, required: false }, warnings);
818
898
  } else {
@@ -821,17 +901,53 @@ function fieldCall(field, typeToIdent, warnings) {
821
901
  }
822
902
  return listOpts ? `m.list(${innerCall}, ${listOpts})` : `m.list(${innerCall})`;
823
903
  }
824
- if (SIMPLE[type]) return scalarCall(SIMPLE[type], field, warnings);
904
+ if (SIMPLE[type]) return scalarCall(SIMPLE[type], field, warnings, filterableEntry(field));
825
905
  warnings.push(`unmapped field type "${type}" on field "${field.key}"`);
826
906
  return `m.json() /* TODO: unmapped type ${type} */`;
827
907
  }
908
+ function accessSource(access) {
909
+ if (!access) return "";
910
+ const parts = [];
911
+ if (access.admin === "MERCHANT_READ_WRITE") parts.push(`admin: "merchant_read_write"`);
912
+ if (access.storefront === "PUBLIC_READ") parts.push(`storefront: "public_read"`);
913
+ if (access.customerAccount === "READ") parts.push(`customerAccount: "read"`);
914
+ return parts.length ? `{ ${parts.join(", ")} }` : "";
915
+ }
916
+ function capabilitiesSource(caps) {
917
+ if (!caps) return "";
918
+ const parts = [];
919
+ if (caps.publishable?.enabled) parts.push(`publishable: true`);
920
+ if (caps.translatable?.enabled) parts.push(`translatable: true`);
921
+ if (caps.renderable?.enabled) {
922
+ const d = caps.renderable.data;
923
+ const dparts = [];
924
+ if (d?.metaTitleKey != null) dparts.push(`metaTitleKey: ${JSON.stringify(d.metaTitleKey)}`);
925
+ if (d?.metaDescriptionKey != null) dparts.push(`metaDescriptionKey: ${JSON.stringify(d.metaDescriptionKey)}`);
926
+ parts.push(dparts.length ? `renderable: { ${dparts.join(", ")} }` : `renderable: true`);
927
+ }
928
+ if (caps.onlineStore?.enabled && caps.onlineStore.data?.urlHandle != null) {
929
+ parts.push(`onlineStore: { urlHandle: ${JSON.stringify(caps.onlineStore.data.urlHandle)} }`);
930
+ }
931
+ return parts.length ? `{ ${parts.join(", ")} }` : "";
932
+ }
933
+ function defConfigLines(def) {
934
+ const lines = [];
935
+ if (isMerchant(def.type)) lines.push(` scope: "merchant",`);
936
+ if (def.name) lines.push(` name: ${JSON.stringify(def.name)},`);
937
+ if (def.description != null) lines.push(` description: ${JSON.stringify(def.description)},`);
938
+ if (def.displayNameKey != null) lines.push(` displayName: ${JSON.stringify(def.displayNameKey)},`);
939
+ const access = accessSource(def.access);
940
+ if (access) lines.push(` access: ${access},`);
941
+ const caps = capabilitiesSource(def.capabilities);
942
+ if (caps) lines.push(` capabilities: ${caps},`);
943
+ return lines.length ? `
944
+ ${lines.join("\n")}` : "";
945
+ }
828
946
  function defSource(def, typeToIdent, warnings) {
829
947
  const ident = typeToIdent.get(def.type);
830
948
  const handle = handleOf(def.type);
831
949
  const fields = def.fields.map((f) => ` ${f.key}: ${fieldCall(f, typeToIdent, warnings)},`).join("\n");
832
- const name = def.name ? `
833
- name: ${JSON.stringify(def.name)},` : "";
834
- return `export const ${ident} = defineMetaobject(${JSON.stringify(handle)}, {${name}
950
+ return `export const ${ident} = defineMetaobject(${JSON.stringify(handle)}, {${defConfigLines(def)}
835
951
  fields: {
836
952
  ${fields}
837
953
  },
@@ -843,6 +959,8 @@ function referencedTypes(def) {
843
959
  if (f.type === "metaobject_reference" || f.type === "list.metaobject_reference") {
844
960
  const t = refTarget(f);
845
961
  if (t) out.add(t);
962
+ } else if (f.type === "mixed_reference" || f.type === "list.mixed_reference") {
963
+ for (const t of refTargets(f)) out.add(t);
846
964
  }
847
965
  }
848
966
  return out;
@@ -891,6 +1009,42 @@ function sameValidations(a, b) {
891
1009
  const norm = (v2) => JSON.stringify([...v2].sort((x, y) => x.name.localeCompare(y.name)));
892
1010
  return norm(a) === norm(b);
893
1011
  }
1012
+ var CAP_KEYS = ["publishable", "translatable", "renderable", "onlineStore"];
1013
+ function accessChanged(local, remote) {
1014
+ for (const key of ["admin", "storefront", "customerAccount"]) {
1015
+ const lv = local[key];
1016
+ if (lv != null && remote?.[key] !== lv) return true;
1017
+ }
1018
+ return false;
1019
+ }
1020
+ function capabilitiesChanged(local, remote) {
1021
+ for (const key of CAP_KEYS) {
1022
+ const lc = local[key];
1023
+ if (!lc) continue;
1024
+ const rc = remote?.[key];
1025
+ if ((rc?.enabled ?? false) !== lc.enabled) return true;
1026
+ const ldata = lc.data;
1027
+ if (ldata) {
1028
+ const rdata = rc?.data;
1029
+ for (const [k, v2] of Object.entries(ldata)) {
1030
+ if (v2 != null && rdata?.[k] !== v2) return true;
1031
+ }
1032
+ }
1033
+ }
1034
+ return false;
1035
+ }
1036
+ function disablesOnlineStore(local, remote) {
1037
+ return local.onlineStore?.enabled === false && remote?.onlineStore?.enabled === true;
1038
+ }
1039
+ function definitionChanges(local, remote) {
1040
+ const changes = [];
1041
+ if (local.name != null && local.name !== remote.name) changes.push("name");
1042
+ if (local.description != null && local.description !== remote.description) changes.push("description");
1043
+ if (local.displayNameKey != null && local.displayNameKey !== remote.displayNameKey) changes.push("displayNameKey");
1044
+ if (local.access && accessChanged(local.access, remote.access)) changes.push("access");
1045
+ if (local.capabilities && capabilitiesChanged(local.capabilities, remote.capabilities)) changes.push("capabilities");
1046
+ return changes;
1047
+ }
894
1048
  function diff(local, remote) {
895
1049
  const ops = [];
896
1050
  const remoteByType = new Map(remote.map((d) => [d.type, d]));
@@ -914,6 +1068,7 @@ function diff(local, remote) {
914
1068
  }
915
1069
  const changes = {};
916
1070
  if (rf.required !== lf.required) changes.required = lf.required;
1071
+ if (rf.filterable !== lf.filterable) changes.filterable = lf.filterable;
917
1072
  if (!sameValidations(rf.validations, lf.validations)) changes.validations = lf.validations;
918
1073
  if (Object.keys(changes).length) {
919
1074
  ops.push({ kind: "updateField", type: localDef.type, key: lf.key, changes });
@@ -924,35 +1079,158 @@ function diff(local, remote) {
924
1079
  ops.push({ kind: "removeField", type: localDef.type, key: rf.key, destructive: true });
925
1080
  }
926
1081
  }
1082
+ const metaChanges = definitionChanges(localDef, remoteDef);
1083
+ if (metaChanges.length) {
1084
+ const destructive = localDef.capabilities ? disablesOnlineStore(localDef.capabilities, remoteDef.capabilities) : false;
1085
+ ops.push({
1086
+ kind: "updateDefinition",
1087
+ type: localDef.type,
1088
+ changes: metaChanges,
1089
+ ...destructive ? { destructive: true } : {}
1090
+ });
1091
+ }
927
1092
  }
928
1093
  return ops;
929
1094
  }
930
1095
 
931
1096
  // src/sync/normalize.ts
932
- function normalizeLocal(schema) {
933
- const def = schema.toDefinitionInput();
934
- return {
1097
+ function localCapabilities(caps) {
1098
+ const out = {};
1099
+ if (caps?.publishable) out.publishable = { enabled: caps.publishable.enabled };
1100
+ if (caps?.translatable) out.translatable = { enabled: caps.translatable.enabled };
1101
+ if (caps?.renderable) {
1102
+ out.renderable = { enabled: caps.renderable.enabled };
1103
+ if (caps.renderable.data) out.renderable.data = { ...caps.renderable.data };
1104
+ }
1105
+ out.onlineStore = caps?.onlineStore ? { enabled: caps.onlineStore.enabled, data: { urlHandle: caps.onlineStore.data?.urlHandle } } : { enabled: false };
1106
+ return out;
1107
+ }
1108
+ function normalizeDefinition(def) {
1109
+ const out = {
935
1110
  type: def.type,
936
1111
  name: def.name,
1112
+ capabilities: localCapabilities(def.capabilities),
937
1113
  fields: def.fieldDefinitions.map((f) => ({
938
1114
  key: f.key,
939
1115
  type: f.type,
940
1116
  required: f.required,
1117
+ filterable: f.capabilities?.adminFilterable?.enabled ?? false,
941
1118
  validations: f.validations
942
1119
  }))
943
1120
  };
1121
+ if (def.description != null) out.description = def.description;
1122
+ if (def.displayNameKey != null) out.displayNameKey = def.displayNameKey;
1123
+ if (def.access) out.access = { ...def.access };
1124
+ return out;
1125
+ }
1126
+ function normalizeLocal(schema) {
1127
+ return normalizeDefinition(schema.toDefinitionInput());
1128
+ }
1129
+ function remoteAccess(access) {
1130
+ if (!access) return void 0;
1131
+ const out = {};
1132
+ if (access.admin != null) out.admin = access.admin;
1133
+ if (access.storefront != null) out.storefront = access.storefront;
1134
+ if (access.customerAccount != null) out.customerAccount = access.customerAccount;
1135
+ return out;
1136
+ }
1137
+ function remoteCapabilities(caps) {
1138
+ const out = {};
1139
+ if (!caps) return out;
1140
+ if (caps.publishable) out.publishable = { enabled: !!caps.publishable.enabled };
1141
+ if (caps.translatable) out.translatable = { enabled: !!caps.translatable.enabled };
1142
+ if (caps.renderable) {
1143
+ out.renderable = { enabled: !!caps.renderable.enabled };
1144
+ const d = caps.renderable.data;
1145
+ if (d) {
1146
+ const data = {};
1147
+ if (d.metaTitleKey != null) data.metaTitleKey = d.metaTitleKey;
1148
+ if (d.metaDescriptionKey != null) data.metaDescriptionKey = d.metaDescriptionKey;
1149
+ if (Object.keys(data).length) out.renderable.data = data;
1150
+ }
1151
+ }
1152
+ if (caps.onlineStore) {
1153
+ out.onlineStore = { enabled: !!caps.onlineStore.enabled };
1154
+ if (caps.onlineStore.data?.urlHandle != null) out.onlineStore.data = { urlHandle: caps.onlineStore.data.urlHandle };
1155
+ }
1156
+ return out;
944
1157
  }
945
1158
  function normalizeRemote(def) {
946
- return {
1159
+ const out = {
947
1160
  type: def.type,
948
1161
  name: def.name,
1162
+ capabilities: remoteCapabilities(def.capabilities),
949
1163
  fields: def.fieldDefinitions.map((f) => ({
950
1164
  key: f.key,
951
1165
  type: typeof f.type === "string" ? f.type : f.type.name,
952
1166
  required: f.required,
1167
+ filterable: f.capabilities?.adminFilterable?.enabled ?? false,
953
1168
  validations: f.validations ?? []
954
1169
  }))
955
1170
  };
1171
+ if (def.description != null) out.description = def.description;
1172
+ if (def.displayNameKey != null) out.displayNameKey = def.displayNameKey;
1173
+ const access = remoteAccess(def.access);
1174
+ if (access) out.access = access;
1175
+ return out;
1176
+ }
1177
+
1178
+ // src/sync/resolve.ts
1179
+ var APP_PREFIX2 = "$app:";
1180
+ function effectiveScope(schema, config) {
1181
+ return schema.config.scope ?? config.scope ?? "app";
1182
+ }
1183
+ function effectiveType(handle, scope) {
1184
+ return scope === "merchant" ? handle : `${APP_PREFIX2}${handle}`;
1185
+ }
1186
+ function rewriteReference(v2, effByCanonical) {
1187
+ if (v2.name === "metaobject_definition_type") {
1188
+ const mapped = effByCanonical.get(v2.value);
1189
+ return mapped ? { ...v2, value: mapped } : v2;
1190
+ }
1191
+ if (v2.name === "metaobject_definition_types") {
1192
+ try {
1193
+ const parsed = JSON.parse(v2.value);
1194
+ if (Array.isArray(parsed)) {
1195
+ const rewritten = parsed.map((t) => typeof t === "string" ? effByCanonical.get(t) ?? t : t);
1196
+ return { ...v2, value: JSON.stringify(rewritten) };
1197
+ }
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ return v2;
1202
+ }
1203
+ function resolveAdmin(out, handle, scope, config) {
1204
+ const explicitAdmin = out.access?.admin;
1205
+ if (scope === "merchant") {
1206
+ if (explicitAdmin != null) {
1207
+ throw new Error(
1208
+ `"${handle}" is merchant-scoped but sets access.admin; admin access is only valid on app-scoped metaobjects.`
1209
+ );
1210
+ }
1211
+ return;
1212
+ }
1213
+ if (explicitAdmin == null) {
1214
+ const admin = config.merchantEditable ? "MERCHANT_READ_WRITE" : "MERCHANT_READ";
1215
+ out.access = { ...out.access, admin };
1216
+ }
1217
+ }
1218
+ function resolveDefinitions(schemas, config = {}) {
1219
+ const effByCanonical = /* @__PURE__ */ new Map();
1220
+ for (const s of schemas) {
1221
+ effByCanonical.set(`${APP_PREFIX2}${s.handle}`, effectiveType(s.handle, effectiveScope(s, config)));
1222
+ }
1223
+ return schemas.map((s) => {
1224
+ const scope = effectiveScope(s, config);
1225
+ const base = s.toDefinitionInput();
1226
+ const fieldDefinitions = base.fieldDefinitions.map((f) => ({
1227
+ ...f,
1228
+ validations: f.validations.map((v2) => rewriteReference(v2, effByCanonical))
1229
+ }));
1230
+ const out = { ...base, type: effectiveType(s.handle, scope), fieldDefinitions };
1231
+ resolveAdmin(out, s.handle, scope, config);
1232
+ return out;
1233
+ });
956
1234
  }
957
1235
 
958
1236
  // src/sync/client.ts
@@ -970,12 +1248,14 @@ var PULL_DEFINITION_QUERY = `query PullMetaobjectDefinition($type: String!) {
970
1248
  required
971
1249
  type { name }
972
1250
  validations { name value }
1251
+ capabilities { adminFilterable { enabled } }
973
1252
  }
974
- access { admin storefront }
1253
+ access { admin storefront customerAccount }
975
1254
  capabilities {
976
1255
  publishable { enabled }
977
1256
  translatable { enabled }
978
- renderable { enabled }
1257
+ renderable { enabled data { metaTitleKey metaDescriptionKey } }
1258
+ onlineStore { enabled data { urlHandle } }
979
1259
  }
980
1260
  }
981
1261
  }`;
@@ -985,6 +1265,8 @@ var LIST_DEFINITIONS_QUERY = `query ListMetaobjectDefinitions($after: String) {
985
1265
  id
986
1266
  name
987
1267
  type
1268
+ description
1269
+ displayNameKey
988
1270
  fieldDefinitions {
989
1271
  key
990
1272
  name
@@ -992,6 +1274,14 @@ var LIST_DEFINITIONS_QUERY = `query ListMetaobjectDefinitions($after: String) {
992
1274
  required
993
1275
  type { name }
994
1276
  validations { name value }
1277
+ capabilities { adminFilterable { enabled } }
1278
+ }
1279
+ access { admin storefront customerAccount }
1280
+ capabilities {
1281
+ publishable { enabled }
1282
+ translatable { enabled }
1283
+ renderable { enabled data { metaTitleKey metaDescriptionKey } }
1284
+ onlineStore { enabled data { urlHandle } }
995
1285
  }
996
1286
  }
997
1287
  pageInfo { hasNextPage endCursor }
@@ -1026,17 +1316,24 @@ async function execute(client, query, variables) {
1026
1316
  }
1027
1317
 
1028
1318
  // src/sync/pull.ts
1319
+ function toDefinition(type, node) {
1320
+ return {
1321
+ type,
1322
+ name: node.name,
1323
+ description: node.description,
1324
+ displayNameKey: node.displayNameKey,
1325
+ access: node.access,
1326
+ capabilities: node.capabilities,
1327
+ fieldDefinitions: node.fieldDefinitions
1328
+ };
1329
+ }
1029
1330
  async function pull(client, types) {
1030
1331
  const out = [];
1031
1332
  for (const type of types) {
1032
1333
  const data = await execute(client, PULL_DEFINITION_QUERY, { type });
1033
1334
  const node = data.metaobjectDefinitionByType;
1034
1335
  if (!node) continue;
1035
- out.push({
1036
- id: node.id,
1037
- type,
1038
- definition: { type, name: node.name, fieldDefinitions: node.fieldDefinitions }
1039
- });
1336
+ out.push({ id: node.id, type, definition: toDefinition(type, node) });
1040
1337
  }
1041
1338
  return out;
1042
1339
  }
@@ -1054,7 +1351,7 @@ async function pullAll(client, opts = {}) {
1054
1351
  const canonical = toCanonicalType(node.type);
1055
1352
  if (appOwnedOnly && !canonical) continue;
1056
1353
  const type = canonical ?? node.type;
1057
- out.push({ id: node.id, type, definition: { type, name: node.name, fieldDefinitions: node.fieldDefinitions } });
1354
+ out.push({ id: node.id, type, definition: toDefinition(type, node) });
1058
1355
  }
1059
1356
  after = data.metaobjectDefinitions.pageInfo.hasNextPage ? data.metaobjectDefinitions.pageInfo.endCursor : null;
1060
1357
  } while (after !== null);
@@ -1062,28 +1359,57 @@ async function pullAll(client, opts = {}) {
1062
1359
  }
1063
1360
 
1064
1361
  // src/sync/push.ts
1362
+ function definitionUpdateFor(def, changes) {
1363
+ const out = {};
1364
+ for (const c of changes) {
1365
+ if (c === "name") out.name = def.name;
1366
+ else if (c === "description") out.description = def.description;
1367
+ else if (c === "displayNameKey") out.displayNameKey = def.displayNameKey;
1368
+ else if (c === "access") out.access = def.access;
1369
+ else if (c === "capabilities") {
1370
+ const caps = def.capabilities;
1371
+ const payload = {};
1372
+ if (caps?.publishable) payload.publishable = caps.publishable;
1373
+ if (caps?.translatable) payload.translatable = caps.translatable;
1374
+ if (caps?.renderable) payload.renderable = caps.renderable;
1375
+ payload.onlineStore = caps?.onlineStore ?? { enabled: false };
1376
+ out.capabilities = payload;
1377
+ }
1378
+ }
1379
+ return out;
1380
+ }
1065
1381
  function fieldInputFor(defByType, type, key) {
1066
1382
  return defByType.get(type)?.fieldDefinitions.find((f) => f.key === key);
1067
1383
  }
1068
- function referenceEdges(def) {
1384
+ function fieldRefTargets(field) {
1069
1385
  const out = [];
1070
- for (const field of def.fieldDefinitions) {
1071
- for (const v2 of field.validations) {
1072
- if (v2.name === "metaobject_definition_type") {
1073
- out.push(v2.value);
1074
- } else if (v2.name === "metaobject_definition_types") {
1075
- try {
1076
- const parsed = JSON.parse(v2.value);
1077
- if (Array.isArray(parsed)) {
1078
- for (const t of parsed) if (typeof t === "string") out.push(t);
1079
- }
1080
- } catch {
1386
+ for (const v2 of field.validations) {
1387
+ if (v2.name === "metaobject_definition_type") {
1388
+ out.push(v2.value);
1389
+ } else if (v2.name === "metaobject_definition_types") {
1390
+ try {
1391
+ const parsed = JSON.parse(v2.value);
1392
+ if (Array.isArray(parsed)) {
1393
+ for (const t of parsed) if (typeof t === "string") out.push(t);
1081
1394
  }
1395
+ } catch {
1082
1396
  }
1083
1397
  }
1084
1398
  }
1085
1399
  return out;
1086
1400
  }
1401
+ function referenceEdges(def) {
1402
+ return def.fieldDefinitions.flatMap(fieldRefTargets);
1403
+ }
1404
+ function splitCyclicFields(def, cyclicTypes) {
1405
+ const pass1 = [];
1406
+ const deferred = [];
1407
+ for (const f of def.fieldDefinitions) {
1408
+ const breaksCycle = fieldRefTargets(f).some((t) => cyclicTypes.has(t) && t !== def.type);
1409
+ (breaksCycle ? deferred : pass1).push(f);
1410
+ }
1411
+ return { pass1, deferred };
1412
+ }
1087
1413
  function topoSortCreates(types, deps) {
1088
1414
  const remaining = /* @__PURE__ */ new Map();
1089
1415
  const dependents = /* @__PURE__ */ new Map();
@@ -1126,50 +1452,51 @@ async function push(client, plan, sources, options) {
1126
1452
  deps.set(op.type, new Set(targets.filter((t) => createTypes.has(t) && t !== op.type)));
1127
1453
  }
1128
1454
  const { ordered, unordered } = topoSortCreates(createTypes, deps);
1129
- const orderedSet = new Set(ordered);
1130
1455
  const cyclicTypes = new Set(unordered);
1131
1456
  const createByType = new Map(createOps.map((x) => [x.op.type, x]));
1132
- const execOrder = [
1133
- ...ordered.map((t) => createByType.get(t)),
1134
- ...createOps.filter((x) => !orderedSet.has(x.op.type)),
1135
- ...otherOps
1136
- ];
1137
1457
  const failedTypes = /* @__PURE__ */ new Set();
1138
- async function applyOp(op) {
1139
- if (op.kind === "createDefinition") {
1140
- if (cyclicTypes.has(op.type)) {
1141
- failedTypes.add(op.type);
1142
- return { op, status: "blocked", reason: "reference cycle \u2014 two-pass create deferred" };
1143
- }
1144
- for (const dep of deps.get(op.type) ?? []) {
1145
- if (failedTypes.has(dep)) {
1146
- failedTypes.add(op.type);
1147
- return { op, status: "blocked", reason: `blocked: dependency "${dep}" was not created` };
1148
- }
1149
- }
1150
- const def = defByType.get(op.type);
1151
- if (!def) {
1152
- failedTypes.add(op.type);
1153
- return { op, status: "blocked", reason: `no definition input for "${op.type}"` };
1154
- }
1155
- const data2 = await execute(client, CREATE_DEFINITION_MUTATION, { definition: def });
1156
- const payload2 = data2.metaobjectDefinitionCreate;
1157
- if (payload2.userErrors.length) {
1458
+ async function createDefinition(op, definition) {
1459
+ const data = await execute(client, CREATE_DEFINITION_MUTATION, { definition });
1460
+ const payload = data.metaobjectDefinitionCreate;
1461
+ if (payload.userErrors.length) {
1462
+ failedTypes.add(op.type);
1463
+ return { op, status: "failed", userErrors: payload.userErrors };
1464
+ }
1465
+ const id = payload.metaobjectDefinition?.id;
1466
+ if (id) idByType.set(op.type, id);
1467
+ return { op, status: "applied", id };
1468
+ }
1469
+ async function applyAcyclicCreate(op) {
1470
+ for (const dep of deps.get(op.type) ?? []) {
1471
+ if (failedTypes.has(dep)) {
1158
1472
  failedTypes.add(op.type);
1159
- return { op, status: "failed", userErrors: payload2.userErrors };
1473
+ return { op, status: "blocked", reason: `blocked: dependency "${dep}" was not created` };
1160
1474
  }
1161
- const id2 = payload2.metaobjectDefinition?.id;
1162
- if (id2) idByType.set(op.type, id2);
1163
- return { op, status: "applied", id: id2 };
1164
1475
  }
1165
- const destructive = op.kind === "removeField" || op.kind === "changeFieldType";
1476
+ const def = defByType.get(op.type);
1477
+ if (!def) {
1478
+ failedTypes.add(op.type);
1479
+ return { op, status: "blocked", reason: `no definition input for "${op.type}"` };
1480
+ }
1481
+ return createDefinition(op, def);
1482
+ }
1483
+ async function applyFieldOp(op) {
1484
+ const destructive = "destructive" in op && op.destructive === true;
1166
1485
  if (destructive && !allowDestructive) return { op, status: "skipped", reason: "destructive" };
1167
1486
  if (failedTypes.has(op.type)) return { op, status: "blocked", reason: `blocked: definition "${op.type}" was not created` };
1168
1487
  const id = idByType.get(op.type);
1169
1488
  if (id == null) return { op, status: "blocked", reason: `no definition id for "${op.type}"` };
1170
- const fieldDefinitions = fieldOpsFor(op);
1171
- if (!fieldDefinitions) return { op, status: "blocked", reason: `no field input for "${op.type}"` };
1172
- const data = await execute(client, UPDATE_DEFINITION_MUTATION, { id, definition: { fieldDefinitions } });
1489
+ let definition;
1490
+ if (op.kind === "updateDefinition") {
1491
+ const def = defByType.get(op.type);
1492
+ if (!def) return { op, status: "blocked", reason: `no definition input for "${op.type}"` };
1493
+ definition = definitionUpdateFor(def, op.changes);
1494
+ } else {
1495
+ const fieldDefinitions = fieldOpsFor(op);
1496
+ if (!fieldDefinitions) return { op, status: "blocked", reason: `no field input for "${op.type}"` };
1497
+ definition = { fieldDefinitions };
1498
+ }
1499
+ const data = await execute(client, UPDATE_DEFINITION_MUTATION, { id, definition });
1173
1500
  const payload = data.metaobjectDefinitionUpdate;
1174
1501
  if (payload.userErrors.length) return { op, status: "failed", userErrors: payload.userErrors };
1175
1502
  return { op, status: "applied", id: payload.metaobjectDefinition?.id ?? id };
@@ -1190,6 +1517,9 @@ async function push(client, plan, sources, options) {
1190
1517
  validations: field.validations
1191
1518
  };
1192
1519
  if (field.description != null) update.description = field.description;
1520
+ if ("filterable" in op.changes) {
1521
+ update.capabilities = { adminFilterable: { enabled: op.changes.filterable ?? false } };
1522
+ }
1193
1523
  return [{ update }];
1194
1524
  }
1195
1525
  case "removeField":
@@ -1203,7 +1533,53 @@ async function push(client, plan, sources, options) {
1203
1533
  }
1204
1534
  }
1205
1535
  const results = new Array(plan.length);
1206
- for (const { op, index } of execOrder) results[index] = await applyOp(op);
1536
+ for (const type of ordered) {
1537
+ const entry = createByType.get(type);
1538
+ results[entry.index] = await applyAcyclicCreate(entry.op);
1539
+ }
1540
+ const cyclicCreates = createOps.filter((x) => cyclicTypes.has(x.op.type));
1541
+ const deferredByType = /* @__PURE__ */ new Map();
1542
+ for (const { op, index } of cyclicCreates) {
1543
+ const def = defByType.get(op.type);
1544
+ if (!def) {
1545
+ failedTypes.add(op.type);
1546
+ results[index] = { op, status: "blocked", reason: `no definition input for "${op.type}"` };
1547
+ continue;
1548
+ }
1549
+ const failedDep = [...deps.get(op.type) ?? []].find((d) => !cyclicTypes.has(d) && failedTypes.has(d));
1550
+ if (failedDep) {
1551
+ failedTypes.add(op.type);
1552
+ results[index] = { op, status: "blocked", reason: `blocked: dependency "${failedDep}" was not created` };
1553
+ continue;
1554
+ }
1555
+ const { pass1, deferred } = splitCyclicFields(def, cyclicTypes);
1556
+ deferredByType.set(op.type, deferred);
1557
+ results[index] = await createDefinition(op, { ...def, fieldDefinitions: pass1 });
1558
+ }
1559
+ for (const { op, index } of cyclicCreates) {
1560
+ if (results[index]?.status !== "applied") continue;
1561
+ const deferred = deferredByType.get(op.type) ?? [];
1562
+ if (!deferred.length) continue;
1563
+ const failedTarget = deferred.flatMap(fieldRefTargets).find((t) => failedTypes.has(t));
1564
+ if (failedTarget) {
1565
+ failedTypes.add(op.type);
1566
+ results[index] = { op, status: "blocked", reason: `blocked: dependency "${failedTarget}" was not created` };
1567
+ continue;
1568
+ }
1569
+ const id = idByType.get(op.type);
1570
+ if (id == null) {
1571
+ results[index] = { op, status: "blocked", reason: `no definition id for "${op.type}"` };
1572
+ continue;
1573
+ }
1574
+ const definition = { fieldDefinitions: deferred.map((f) => ({ create: f })) };
1575
+ const data = await execute(client, UPDATE_DEFINITION_MUTATION, { id, definition });
1576
+ const payload = data.metaobjectDefinitionUpdate;
1577
+ if (payload.userErrors.length) {
1578
+ failedTypes.add(op.type);
1579
+ results[index] = { op, status: "failed", userErrors: payload.userErrors };
1580
+ }
1581
+ }
1582
+ for (const { op, index } of otherOps) results[index] = await applyFieldOp(op);
1207
1583
  const counts = { applied: 0, skipped: 0, blocked: 0, failed: 0 };
1208
1584
  for (const r of results) counts[r.status]++;
1209
1585
  return { results, counts, ok: counts.failed === 0 && counts.blocked === 0 };
@@ -1218,11 +1594,13 @@ async function push(client, plan, sources, options) {
1218
1594
  diff,
1219
1595
  generateSchemaSource,
1220
1596
  m,
1597
+ normalizeDefinition,
1221
1598
  normalizeLocal,
1222
1599
  normalizeRemote,
1223
1600
  pull,
1224
1601
  pullAll,
1225
1602
  push,
1603
+ resolveDefinitions,
1226
1604
  toDefinitionInput,
1227
1605
  validateConfig
1228
1606
  });