@isardsat/editorial-server 6.16.0 → 6.17.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.
Files changed (2) hide show
  1. package/dist/routes/data.js +169 -1
  2. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
2
- import { EditorialDataItemSchema, EditorialDataSchema, EditorialSchemaSchema, getOptionsReference, } from "@isardsat/editorial-common";
2
+ import { EditorialDataItemSchema, EditorialDataSchema, EditorialDiffResponseSchema, EditorialSchemaSchema, getOptionsReference, } from "@isardsat/editorial-common";
3
3
  import { generateMetaSchema } from "../lib/utils/schema.js";
4
4
  function createCache() {
5
5
  let schemaCache = null;
@@ -117,6 +117,55 @@ function resolveCollectionReferences(collection, schema, itemType, content, orig
117
117
  }
118
118
  return resolvedCollection;
119
119
  }
120
+ /**
121
+ * Compares two items and returns the list of fields that have changed.
122
+ * Excludes metadata fields like 'updatedAt'.
123
+ */
124
+ function getChangedFields(previewItem, productionItem) {
125
+ const changedFields = [];
126
+ const excludedFields = ["updatedAt", "createdAt"];
127
+ // Get all unique keys from both items
128
+ const allKeys = new Set([
129
+ ...Object.keys(previewItem),
130
+ ...Object.keys(productionItem),
131
+ ]);
132
+ for (const key of allKeys) {
133
+ if (excludedFields.includes(key))
134
+ continue;
135
+ const previewValue = previewItem[key];
136
+ const productionValue = productionItem[key];
137
+ if (!deepEqual(previewValue, productionValue)) {
138
+ changedFields.push(key);
139
+ }
140
+ }
141
+ return changedFields;
142
+ }
143
+ /**
144
+ * Deep equality comparison for values.
145
+ */
146
+ function deepEqual(value1, value2) {
147
+ if (value1 === value2)
148
+ return true;
149
+ if (value1 == null || value2 == null)
150
+ return false;
151
+ if (typeof value1 !== typeof value2)
152
+ return false;
153
+ if (typeof value1 !== "object") {
154
+ return value1 === value2;
155
+ }
156
+ if (Array.isArray(value1) !== Array.isArray(value2))
157
+ return false;
158
+ if (Array.isArray(value1)) {
159
+ if (value1.length !== value2.length)
160
+ return false;
161
+ return value1.every((item, index) => deepEqual(item, value2[index]));
162
+ }
163
+ const keys1 = Object.keys(value1);
164
+ const keys2 = Object.keys(value2);
165
+ if (keys1.length !== keys2.length)
166
+ return false;
167
+ return keys1.every((key) => deepEqual(value1[key], value2[key]));
168
+ }
120
169
  export function createDataRoutes(config, storage) {
121
170
  const app = new OpenAPIHono();
122
171
  const cache = createCache();
@@ -553,5 +602,124 @@ export function createDataRoutes(config, storage) {
553
602
  const metaSchema = generateMetaSchema({ allowedExtraFields });
554
603
  return c.json(metaSchema);
555
604
  });
605
+ app.openapi(createRoute({
606
+ method: "get",
607
+ path: "/diff",
608
+ summary: "Get differences between preview and production data",
609
+ responses: {
610
+ 200: {
611
+ content: {
612
+ "application/json": {
613
+ schema: EditorialDiffResponseSchema,
614
+ },
615
+ },
616
+ description: "Get differences between preview and production data",
617
+ },
618
+ },
619
+ tags: ["Data"],
620
+ }), async (c) => {
621
+ // Fetch both preview and production content
622
+ const [previewContent, productionContent, schema] = await Promise.all([
623
+ cache.getContent(storage, { production: false }),
624
+ cache.getContent(storage, { production: true }),
625
+ cache.getSchema(storage),
626
+ ]);
627
+ const result = {
628
+ collections: {},
629
+ singles: {},
630
+ };
631
+ // Get all item types from both preview and production
632
+ const allItemTypes = new Set([
633
+ ...Object.keys(previewContent),
634
+ ...Object.keys(productionContent),
635
+ ]);
636
+ for (const itemType of allItemTypes) {
637
+ const previewCollection = previewContent[itemType] || {};
638
+ const productionCollection = productionContent[itemType] || {};
639
+ const itemSchema = schema[itemType];
640
+ // Check if this is a "singleton" type (only one item allowed)
641
+ const isSingleton = itemSchema?.singleton === true;
642
+ if (isSingleton) {
643
+ // Compare the singleton item
644
+ const previewKeys = Object.keys(previewCollection);
645
+ const productionKeys = Object.keys(productionCollection);
646
+ const singletonKey = previewKeys[0] || productionKeys[0];
647
+ if (singletonKey) {
648
+ const previewItem = previewCollection[singletonKey];
649
+ const productionItem = productionCollection[singletonKey];
650
+ if (previewItem && !productionItem) {
651
+ // Singleton exists in preview but not in production = added
652
+ result.singles[itemType] = {
653
+ status: "added",
654
+ preview: previewItem,
655
+ updatedAt: previewItem.updatedAt,
656
+ changedFields: Object.keys(previewItem).filter((k) => !["updatedAt", "createdAt"].includes(k)),
657
+ };
658
+ }
659
+ else if (!previewItem && productionItem) {
660
+ // Singleton exists in production but not in preview = deleted
661
+ result.singles[itemType] = {
662
+ status: "deleted",
663
+ production: productionItem,
664
+ };
665
+ }
666
+ else if (previewItem?.updatedAt !== productionItem?.updatedAt) {
667
+ // Singleton has different updatedAt = modified
668
+ const changedFields = getChangedFields(previewItem, productionItem);
669
+ result.singles[itemType] = {
670
+ status: "modified",
671
+ preview: previewItem,
672
+ production: productionItem,
673
+ updatedAt: previewItem?.updatedAt,
674
+ changedFields,
675
+ };
676
+ }
677
+ }
678
+ }
679
+ else {
680
+ // Handle collections
681
+ const added = [];
682
+ const modified = [];
683
+ const deleted = [];
684
+ // Find added and modified items
685
+ for (const [id, previewItem] of Object.entries(previewCollection)) {
686
+ const productionItem = productionCollection[id];
687
+ if (!productionItem) {
688
+ // Item exists in preview but not in production = added
689
+ added.push({
690
+ id,
691
+ item: previewItem,
692
+ updatedAt: previewItem.updatedAt,
693
+ });
694
+ }
695
+ else if (previewItem.updatedAt !== productionItem.updatedAt) {
696
+ // Item has different updatedAt = modified (not yet published)
697
+ const changedFields = getChangedFields(previewItem, productionItem);
698
+ modified.push({
699
+ id,
700
+ preview: previewItem,
701
+ production: productionItem,
702
+ updatedAt: previewItem.updatedAt,
703
+ changedFields,
704
+ });
705
+ }
706
+ }
707
+ // Find deleted items (exist in production but not in preview)
708
+ for (const [id, productionItem] of Object.entries(productionCollection)) {
709
+ if (!previewCollection[id]) {
710
+ deleted.push({
711
+ id,
712
+ item: productionItem,
713
+ });
714
+ }
715
+ }
716
+ // Only include collections with changes
717
+ if (added.length > 0 || modified.length > 0 || deleted.length > 0) {
718
+ result.collections[itemType] = { added, modified, deleted };
719
+ }
720
+ }
721
+ }
722
+ return c.json(result);
723
+ });
556
724
  return app;
557
725
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isardsat/editorial-server",
3
- "version": "6.16.0",
3
+ "version": "6.17.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "hono": "^4.9.8",
15
15
  "yaml": "^2.8.1",
16
16
  "zod": "^4.1.11",
17
- "@isardsat/editorial-admin": "^6.16.0",
18
- "@isardsat/editorial-common": "^6.16.0"
17
+ "@isardsat/editorial-common": "^6.17.0",
18
+ "@isardsat/editorial-admin": "^6.17.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@tsconfig/node22": "^22.0.0",