@bobbykim/manguito-cms-api 0.6.0 → 0.8.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.
@@ -28,6 +28,7 @@ declare function createGraphQLHandler(registry: SchemaRegistry, repos: Record<st
28
28
  projection?: VersionProjection;
29
29
  currentProjection?: VersionProjection;
30
30
  currentVersion?: string;
31
+ currentFieldKeyMaps?: Record<string, FieldKeyMap>;
31
32
  }): Handler;
32
33
 
33
34
  export { type ResolvedGraphQLOptions, createGraphQLHandler };
@@ -39,6 +39,7 @@ var import_graphql_yoga = require("graphql-yoga");
39
39
 
40
40
  // src/graphql/schema.ts
41
41
  var import_graphql5 = require("graphql");
42
+ var import_manguito_cms_core = require("@bobbykim/manguito-cms-core");
42
43
 
43
44
  // src/graphql/scalars.ts
44
45
  var import_graphql = require("graphql");
@@ -269,29 +270,22 @@ function scalarFieldResolver(field, fallback) {
269
270
  function relationFieldResolver(typeName, schemaFieldName) {
270
271
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
271
272
  }
272
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
273
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
273
274
  return async (parent, _args, ctx) => {
274
275
  let p = ctx.programmaticMemo.get(parent);
275
276
  if (!p) {
276
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
277
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
277
278
  ctx.programmaticMemo.set(parent, p);
278
279
  }
279
280
  return (await p)[schemaFieldName];
280
281
  };
281
282
  }
282
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
283
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
283
284
  const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
284
- if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
285
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
285
286
  const enriched = { ...parent };
286
- await Promise.all(mediaFields.map((m) => ctx.loaders.load(typeName, m.name, enriched)));
287
- return ctx.resolver.resolveItem(typeName, relabelMedia(toLabels(enriched), enriched, mediaFields));
288
- }
289
- function relabelMedia(record, enriched, mediaFields) {
290
- for (const { name, exposedAs } of mediaFields) {
291
- if (!(name in enriched)) continue;
292
- record[exposedAs] = enriched[name];
293
- }
294
- return record;
287
+ await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
288
+ return ctx.resolver.resolveItem(typeName, toLabels(enriched));
295
289
  }
296
290
  function collectionResolver(typeName, nameMap, fieldKeys) {
297
291
  return async (_root, args, ctx) => {
@@ -365,7 +359,7 @@ var MEDIA = new import_graphql5.GraphQLObjectType({
365
359
  duration: { type: import_graphql5.GraphQLInt, resolve: (m) => m.duration }
366
360
  }
367
361
  });
368
- function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
362
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view, programmaticKeyMaps = fieldKeyMaps) {
369
363
  const objectTypes = /* @__PURE__ */ new Map();
370
364
  const enumTypes = /* @__PURE__ */ new Map();
371
365
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -405,7 +399,8 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
405
399
  `
406
400
  );
407
401
  }
408
- return new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(target ?? MEDIA));
402
+ const t = target ?? MEDIA;
403
+ return (0, import_manguito_cms_core.relationCardinality)(field) === "one" ? t : new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t));
409
404
  }
410
405
  if (field.field_type === "reference") {
411
406
  const ref = field.ui_component.component === "typeahead-select" ? field.ui_component.ref : void 0;
@@ -414,8 +409,7 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
414
409
  process.stderr.write(`\u26A0 reference field '${field.name}' targets unknown type '${ref}'; exposing as Media
415
410
  `);
416
411
  }
417
- const rel = field.ui_component.component === "typeahead-select" ? field.ui_component.rel : void 0;
418
- const isMany = rel === "many-to-many" || rel === "one-to-many";
412
+ const isMany = (0, import_manguito_cms_core.relationCardinality)(field) === "many";
419
413
  const t = target ?? MEDIA;
420
414
  return isMany ? new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t)) : t;
421
415
  }
@@ -429,9 +423,9 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
429
423
  }
430
424
  function buildObjectType(machineName, type) {
431
425
  const visible = viewFieldsFor(machineName, type);
432
- const mediaFields = visible.filter(
433
- (v) => v.field.field_type === "image" || v.field.field_type === "video" || v.field.field_type === "file"
434
- ).map((v) => ({ name: v.field.name, exposedAs: v.exposedAs }));
426
+ const mediaFieldNames = type.fields.filter(
427
+ (f) => f.removed !== true && (f.field_type === "image" || f.field_type === "video" || f.field_type === "file")
428
+ ).map((f) => f.name);
435
429
  return new import_graphql5.GraphQLObjectType({
436
430
  name: graphqlTypeName(machineName),
437
431
  fields: () => {
@@ -451,7 +445,7 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
451
445
  const outType = outputTypeForField(field, vf.required);
452
446
  let resolve;
453
447
  if (field.field_type === "programmatic") {
454
- resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
448
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName]);
455
449
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
456
450
  resolve = relationFieldResolver(machineName, field.name);
457
451
  } else {
@@ -496,7 +490,10 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
496
490
  for (const [name, ct] of Object.entries(registry.content_types)) {
497
491
  const objType = objectTypes.get(name);
498
492
  const visible = viewFieldsFor(name, ct);
499
- const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
493
+ const nameMap = buildFieldNameMap([
494
+ ...visible.map((v) => v.exposedAs),
495
+ ...ct.system_fields.map((s) => s.name)
496
+ ]);
500
497
  if (ct.only_one) {
501
498
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
502
499
  continue;
@@ -625,10 +622,14 @@ var import_dataloader = __toESM(require("dataloader"));
625
622
  // src/relations.ts
626
623
  var import_node_crypto = __toESM(require("crypto"));
627
624
  var import_drizzle_orm = require("drizzle-orm");
625
+ var import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
628
626
  function quoteIdent(name) {
629
627
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
630
628
  return `"${name}"`;
631
629
  }
630
+ function oneOrMany(cardinality, list) {
631
+ return cardinality === "one" ? list[0] ?? null : list;
632
+ }
632
633
  function buildRelationsMap(fields, registry) {
633
634
  const relations = {};
634
635
  for (const field of fields) {
@@ -639,7 +640,11 @@ function buildRelationsMap(fields, registry) {
639
640
  const ref = field.ui_component.ref;
640
641
  const pType = ref ? registry.paragraph_types[ref] : void 0;
641
642
  if (!pType) continue;
642
- relations[field.name] = { type: "paragraph", table: pType.db.table_name };
643
+ relations[field.name] = {
644
+ type: "paragraph",
645
+ table: pType.db.table_name,
646
+ cardinality: (0, import_manguito_cms_core2.relationCardinality)(field) ?? "many"
647
+ };
643
648
  } else if (field.field_type === "reference") {
644
649
  if (!field.db_column) continue;
645
650
  if (field.db_column.junction) {
@@ -672,9 +677,8 @@ function groupBy(items, key) {
672
677
  }
673
678
  return result;
674
679
  }
675
- function needsFkResolution(row, fkColumn, fieldName) {
676
- if (fkColumn !== fieldName && !(fkColumn in row)) return false;
677
- const current = row[fieldName];
680
+ function needsFkResolution(row, fkColumn) {
681
+ const current = row[fkColumn];
678
682
  return !(typeof current === "object" && current !== null);
679
683
  }
680
684
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
@@ -688,18 +692,14 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
688
692
  );
689
693
  const byParent = groupBy(result.rows, "parent_id");
690
694
  for (const row of batch) {
691
- row[fieldName] = byParent[row["id"]] ?? [];
695
+ row[fieldName] = oneOrMany(rel.cardinality, byParent[row["id"]] ?? []);
692
696
  }
693
697
  } else if (rel.type === "reference") {
694
- const dropFk = rel.fk_column !== fieldName;
695
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
698
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
696
699
  if (pending.length === 0) return;
697
700
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
698
701
  if (fkValues.length === 0) {
699
- for (const row of pending) {
700
- row[fieldName] = null;
701
- if (dropFk) delete row[rel.fk_column];
702
- }
702
+ for (const row of pending) row[rel.fk_column] = null;
703
703
  return;
704
704
  }
705
705
  const unique = [...new Set(fkValues)];
@@ -716,8 +716,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
716
716
  }
717
717
  for (const row of pending) {
718
718
  const fkVal = row[rel.fk_column];
719
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
720
- if (dropFk) delete row[rel.fk_column];
719
+ row[rel.fk_column] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
721
720
  }
722
721
  } else if (rel.type === "junction") {
723
722
  const parentIds = batch.map((r) => r["id"]);
@@ -745,15 +744,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
745
744
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
746
745
  }
747
746
  } else if (rel.type === "media") {
748
- const dropFk = rel.fk_column !== fieldName;
749
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
747
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
750
748
  if (pending.length === 0) return;
751
749
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
752
750
  if (fkValues.length === 0) {
753
- for (const row of pending) {
754
- row[fieldName] = null;
755
- if (dropFk) delete row[rel.fk_column];
756
- }
751
+ for (const row of pending) row[rel.fk_column] = null;
757
752
  return;
758
753
  }
759
754
  const unique = [...new Set(fkValues)];
@@ -767,8 +762,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
767
762
  }
768
763
  for (const row of pending) {
769
764
  const fkVal = row[rel.fk_column];
770
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
771
- if (dropFk) delete row[rel.fk_column];
765
+ row[rel.fk_column] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
772
766
  }
773
767
  }
774
768
  }
@@ -795,7 +789,8 @@ function createRelationLoaders(db, registry) {
795
789
  async (parents) => {
796
790
  const rows = parents;
797
791
  await resolveRelationField(db, rows, fieldName, rel, cache, true);
798
- return rows.map((r) => r[fieldName]);
792
+ const resultKey = rel.type === "reference" || rel.type === "media" ? rel.fk_column : fieldName;
793
+ return rows.map((r) => r[resultKey]);
799
794
  },
800
795
  // Batch within a tick; do not memoize by parent identity (rows are mutated
801
796
  // and may recur across nesting levels).
@@ -862,7 +857,7 @@ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, optio
862
857
  currentVersion: versioning.currentVersion
863
858
  });
864
859
  }
865
- const schema = buildGraphQLSchema(registry, fieldKeyMaps, view);
860
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps);
866
861
  const { plugins } = buildArmorPlugin({
867
862
  maxDepth: options.maxDepth,
868
863
  maxComplexity: options.maxComplexity