@bobbykim/manguito-cms-api 0.5.0 → 0.7.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.
@@ -148,10 +148,13 @@ var SORTABLE = [
148
148
  { gql: "createdAt", value: "created_at" },
149
149
  { gql: "updatedAt", value: "updated_at" }
150
150
  ];
151
- function buildSortFieldEnum(typeName) {
151
+ function buildSortFieldEnum(typeName, fieldKeys) {
152
+ const resolvable = SORTABLE.filter(
153
+ (s) => s.gql === "createdAt" || s.gql === "updatedAt" || fieldKeys === void 0 || fieldKeys.columnFor(s.value) !== void 0
154
+ );
152
155
  return new import_graphql3.GraphQLEnumType({
153
156
  name: `${typeName}SortField`,
154
- values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
157
+ values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }]))
155
158
  });
156
159
  }
157
160
  function comparableFilterInput(name, scalar) {
@@ -195,18 +198,19 @@ function filterInputForField(fieldType) {
195
198
  if (scalar === null) return null;
196
199
  return StringFilter;
197
200
  }
198
- function buildFilterInputType(type) {
201
+ function buildFilterInputType(type, viewFields) {
199
202
  const fields = {};
200
203
  fields["id"] = { type: IDFilter };
201
204
  fields["published"] = { type: BooleanFilter };
202
205
  fields["createdAt"] = { type: DateTimeFilter };
203
206
  fields["updatedAt"] = { type: DateTimeFilter };
204
207
  if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
205
- for (const f of type.fields) {
208
+ const entries = viewFields !== void 0 ? viewFields.map((v) => ({ field: v.field, exposedAs: v.exposedAs })) : type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name }));
209
+ for (const { field: f, exposedAs } of entries) {
206
210
  if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
207
211
  if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
208
212
  const input = filterInputForField(f.field_type);
209
- if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
213
+ if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input };
210
214
  }
211
215
  if (Object.keys(fields).length === 0) return null;
212
216
  return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
@@ -253,12 +257,14 @@ function codeError(code, message) {
253
257
  function isPublished(item) {
254
258
  return !!item && item["published"] === true;
255
259
  }
256
- function resolveFieldValue(field, row) {
260
+ function resolveFieldValue(field, row, fallback) {
257
261
  const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
258
- return row[key] ?? null;
262
+ const value = row[key];
263
+ if ((value === null || value === void 0) && fallback !== void 0) return fallback;
264
+ return value ?? null;
259
265
  }
260
- function scalarFieldResolver(field) {
261
- return (parent) => resolveFieldValue(field, parent);
266
+ function scalarFieldResolver(field, fallback) {
267
+ return (parent) => resolveFieldValue(field, parent, fallback);
262
268
  }
263
269
  function relationFieldResolver(typeName, schemaFieldName) {
264
270
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
@@ -352,7 +358,7 @@ var MEDIA = new import_graphql5.GraphQLObjectType({
352
358
  duration: { type: import_graphql5.GraphQLInt, resolve: (m) => m.duration }
353
359
  }
354
360
  });
355
- function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
361
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view, programmaticKeyMaps = fieldKeyMaps) {
356
362
  const objectTypes = /* @__PURE__ */ new Map();
357
363
  const enumTypes = /* @__PURE__ */ new Map();
358
364
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -371,14 +377,14 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
371
377
  );
372
378
  }
373
379
  }
374
- function outputTypeForField(field) {
380
+ function outputTypeForField(field, required) {
375
381
  const scalar = scalarOutputType(field.field_type);
376
- if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
382
+ if (scalar) return required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
377
383
  if (field.field_type === "enum") {
378
384
  const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
379
385
  const et = ref ? enumTypes.get(ref) : void 0;
380
386
  const t = et ?? import_graphql5.GraphQLString;
381
- return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
387
+ return required ? new import_graphql5.GraphQLNonNull(t) : t;
382
388
  }
383
389
  if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
384
390
  return MEDIA;
@@ -408,8 +414,17 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
408
414
  }
409
415
  return import_graphql5.GraphQLString;
410
416
  }
417
+ function exposedFields(type) {
418
+ return type.fields.filter((f) => f.removed !== true);
419
+ }
420
+ function viewFieldsFor(machineName, type) {
421
+ return view?.[machineName]?.fields ?? exposedFields(type).map((f) => ({ field: f, exposedAs: f.name, required: f.required }));
422
+ }
411
423
  function buildObjectType(machineName, type) {
412
- const mediaFieldNames = type.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name);
424
+ const visible = viewFieldsFor(machineName, type);
425
+ const mediaFieldNames = type.fields.filter(
426
+ (f) => f.removed !== true && (f.field_type === "image" || f.field_type === "video" || f.field_type === "file")
427
+ ).map((f) => f.name);
413
428
  return new import_graphql5.GraphQLObjectType({
414
429
  name: graphqlTypeName(machineName),
415
430
  fields: () => {
@@ -423,23 +438,25 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
423
438
  }
424
439
  fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
425
440
  fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
426
- for (const field of type.fields) {
427
- const gqlName = toCamelCase(field.name);
428
- const outType = outputTypeForField(field);
441
+ for (const vf of visible) {
442
+ const field = vf.field;
443
+ const gqlName = toCamelCase(vf.exposedAs);
444
+ const outType = outputTypeForField(field, vf.required);
429
445
  let resolve;
430
446
  if (field.field_type === "programmatic") {
431
- resolve = programmaticFieldResolver(
432
- machineName,
433
- field.name,
434
- mediaFieldNames,
435
- fieldKeyMaps[machineName]
436
- );
447
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName]);
437
448
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
438
449
  resolve = relationFieldResolver(machineName, field.name);
439
450
  } else {
440
- resolve = scalarFieldResolver(field);
451
+ resolve = scalarFieldResolver(field, vf.fallback);
441
452
  }
442
- fields[gqlName] = { type: outType, resolve };
453
+ fields[gqlName] = {
454
+ type: outType,
455
+ resolve,
456
+ // Conditional spread, not `deprecationReason: vf.deprecationReason`
457
+ // — exactOptionalPropertyTypes forbids present-and-undefined.
458
+ ...vf.deprecationReason !== void 0 && { deprecationReason: vf.deprecationReason }
459
+ };
443
460
  }
444
461
  return fields;
445
462
  }
@@ -471,7 +488,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
471
488
  const queryFields = {};
472
489
  for (const [name, ct] of Object.entries(registry.content_types)) {
473
490
  const objType = objectTypes.get(name);
474
- const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
491
+ const visible = viewFieldsFor(name, ct);
492
+ const nameMap = buildFieldNameMap([
493
+ ...visible.map((v) => v.exposedAs),
494
+ ...ct.system_fields.map((s) => s.name)
495
+ ]);
475
496
  if (ct.only_one) {
476
497
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
477
498
  continue;
@@ -483,13 +504,13 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
483
504
  meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
484
505
  }
485
506
  });
486
- const filterType = buildFilterInputType(ct);
507
+ const filterType = buildFilterInputType(ct, visible);
487
508
  queryFields[collectionQueryName(name)] = {
488
509
  type: new import_graphql5.GraphQLNonNull(listType),
489
510
  args: {
490
511
  page: { type: import_graphql5.GraphQLInt },
491
512
  perPage: { type: import_graphql5.GraphQLInt },
492
- sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
513
+ sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },
493
514
  sortOrder: { type: SortOrderEnum },
494
515
  ...filterType ? { filter: { type: filterType } } : {}
495
516
  },
@@ -513,7 +534,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
513
534
  queryFields[collectionQueryName(name)] = {
514
535
  type: new import_graphql5.GraphQLNonNull(listType),
515
536
  args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
516
- resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
537
+ resolve: collectionResolver(
538
+ name,
539
+ buildFieldNameMap(viewFieldsFor(name, tt).map((v) => v.exposedAs)),
540
+ fieldKeyMaps[name]
541
+ )
517
542
  };
518
543
  queryFields[singleQueryName(name)] = {
519
544
  type: objType,
@@ -526,6 +551,70 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
526
551
  });
527
552
  }
528
553
 
554
+ // src/graphql/version-view.ts
555
+ function byColumn(fields) {
556
+ const out = /* @__PURE__ */ new Map();
557
+ for (const f of fields) {
558
+ if (!isColumnBacked(f)) continue;
559
+ out.set(f.db_column.column_name, f);
560
+ }
561
+ return out;
562
+ }
563
+ function deprecationFor(field, currentLabels, currentVersion) {
564
+ const currentLabel = currentLabels.get(field.column_name);
565
+ if (currentLabel === void 0) {
566
+ return `Removed in ${currentVersion}; column retained while this version is live.`;
567
+ }
568
+ if (currentLabel !== field.exposed_as) {
569
+ return `Renamed to '${currentLabel}' in ${currentVersion}.`;
570
+ }
571
+ return void 0;
572
+ }
573
+ function buildVersionView(input) {
574
+ const { registry, projection, currentProjection, currentVersion } = input;
575
+ const out = {};
576
+ const sources = [
577
+ registry.content_types,
578
+ registry.taxonomy_types,
579
+ registry.paragraph_types
580
+ ];
581
+ for (const source of sources) {
582
+ for (const [typeName, type] of Object.entries(source)) {
583
+ const projectionType = projection?.types[typeName];
584
+ if (projectionType === void 0) {
585
+ out[typeName] = {
586
+ fields: type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name, required: f.required }))
587
+ };
588
+ continue;
589
+ }
590
+ const columns = byColumn(type.fields);
591
+ const currentLabels = /* @__PURE__ */ new Map();
592
+ for (const f of currentProjection?.types[typeName]?.fields ?? []) {
593
+ currentLabels.set(f.column_name, f.exposed_as);
594
+ }
595
+ const fields = [];
596
+ for (const pf of projectionType.fields) {
597
+ const field = columns.get(pf.column_name);
598
+ if (field === void 0) continue;
599
+ const reason = deprecationFor(pf, currentLabels, currentVersion);
600
+ fields.push({
601
+ field,
602
+ exposedAs: pf.exposed_as,
603
+ required: pf.required,
604
+ ...pf.fallback !== void 0 && { fallback: pf.fallback },
605
+ ...reason !== void 0 && { deprecationReason: reason }
606
+ });
607
+ }
608
+ for (const f of type.fields) {
609
+ if (isColumnBacked(f) || f.removed === true) continue;
610
+ fields.push({ field: f, exposedAs: f.name, required: f.required });
611
+ }
612
+ out[typeName] = { fields };
613
+ }
614
+ }
615
+ return out;
616
+ }
617
+
529
618
  // src/graphql/dataloaders.ts
530
619
  var import_dataloader = __toESM(require("dataloader"));
531
620
 
@@ -579,9 +668,8 @@ function groupBy(items, key) {
579
668
  }
580
669
  return result;
581
670
  }
582
- function needsFkResolution(row, fkColumn, fieldName) {
583
- if (fkColumn !== fieldName && !(fkColumn in row)) return false;
584
- const current = row[fieldName];
671
+ function needsFkResolution(row, fkColumn) {
672
+ const current = row[fkColumn];
585
673
  return !(typeof current === "object" && current !== null);
586
674
  }
587
675
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
@@ -598,15 +686,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
598
686
  row[fieldName] = byParent[row["id"]] ?? [];
599
687
  }
600
688
  } else if (rel.type === "reference") {
601
- const dropFk = rel.fk_column !== fieldName;
602
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
689
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
603
690
  if (pending.length === 0) return;
604
691
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
605
692
  if (fkValues.length === 0) {
606
- for (const row of pending) {
607
- row[fieldName] = null;
608
- if (dropFk) delete row[rel.fk_column];
609
- }
693
+ for (const row of pending) row[rel.fk_column] = null;
610
694
  return;
611
695
  }
612
696
  const unique = [...new Set(fkValues)];
@@ -623,8 +707,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
623
707
  }
624
708
  for (const row of pending) {
625
709
  const fkVal = row[rel.fk_column];
626
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
627
- if (dropFk) delete row[rel.fk_column];
710
+ row[rel.fk_column] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
628
711
  }
629
712
  } else if (rel.type === "junction") {
630
713
  const parentIds = batch.map((r) => r["id"]);
@@ -652,15 +735,11 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
652
735
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
653
736
  }
654
737
  } else if (rel.type === "media") {
655
- const dropFk = rel.fk_column !== fieldName;
656
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
738
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
657
739
  if (pending.length === 0) return;
658
740
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
659
741
  if (fkValues.length === 0) {
660
- for (const row of pending) {
661
- row[fieldName] = null;
662
- if (dropFk) delete row[rel.fk_column];
663
- }
742
+ for (const row of pending) row[rel.fk_column] = null;
664
743
  return;
665
744
  }
666
745
  const unique = [...new Set(fkValues)];
@@ -674,8 +753,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
674
753
  }
675
754
  for (const row of pending) {
676
755
  const fkVal = row[rel.fk_column];
677
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
678
- if (dropFk) delete row[rel.fk_column];
756
+ row[rel.fk_column] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
679
757
  }
680
758
  }
681
759
  }
@@ -702,7 +780,8 @@ function createRelationLoaders(db, registry) {
702
780
  async (parents) => {
703
781
  const rows = parents;
704
782
  await resolveRelationField(db, rows, fieldName, rel, cache, true);
705
- return rows.map((r) => r[fieldName]);
783
+ const resultKey = rel.type === "reference" || rel.type === "media" ? rel.fk_column : fieldName;
784
+ return rows.map((r) => r[resultKey]);
706
785
  },
707
786
  // Batch within a tick; do not memoize by parent identity (rows are mutated
708
787
  // and may recur across nesting levels).
@@ -754,15 +833,29 @@ function isClientSafeGraphQLError(error) {
754
833
  }
755
834
  return false;
756
835
  }
757
- function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
758
- const schema = buildGraphQLSchema(registry, fieldKeyMaps);
836
+ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options, versioning = {}) {
837
+ let view;
838
+ if (versioning.projection !== void 0) {
839
+ if (versioning.currentVersion === void 0) {
840
+ throw new Error(
841
+ 'createGraphQLHandler: currentVersion is required when a projection is supplied \u2014 deprecation reasons name it, and an empty one ships "Removed in ." into published SDL.'
842
+ );
843
+ }
844
+ view = buildVersionView({
845
+ registry,
846
+ projection: versioning.projection,
847
+ currentProjection: versioning.currentProjection,
848
+ currentVersion: versioning.currentVersion
849
+ });
850
+ }
851
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps);
759
852
  const { plugins } = buildArmorPlugin({
760
853
  maxDepth: options.maxDepth,
761
854
  maxComplexity: options.maxComplexity
762
855
  });
763
856
  const yoga = (0, import_graphql_yoga.createYoga)({
764
857
  schema,
765
- graphqlEndpoint: "/graphql",
858
+ graphqlEndpoint: versioning.endpoint ?? "/graphql",
766
859
  graphiql: options.graphiql,
767
860
  landingPage: false,
768
861
  maskedErrors: {