@isardsat/editorial-server 6.19.5 → 6.21.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/lib/storage.d.ts +6 -2
- package/dist/lib/storage.js +19 -1
- package/dist/lib/utils/references.d.ts +19 -0
- package/dist/lib/utils/references.js +135 -0
- package/dist/lib/utils/version.d.ts +2 -0
- package/dist/lib/utils/version.js +22 -0
- package/dist/routes/app.d.ts +1 -0
- package/dist/routes/app.js +1 -0
- package/dist/routes/data.js +71 -10
- package/dist/routes/version.d.ts +3 -0
- package/dist/routes/version.js +69 -0
- package/package.json +3 -3
package/dist/lib/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EditorialData, EditorialDataObjectWithType } from "@isardsat/editorial-common";
|
|
1
|
+
import type { EditorialData, EditorialDataObjectWithType, EditorialUpdateDataItem } from "@isardsat/editorial-common";
|
|
2
2
|
export declare function createStorage(dataDirectory: string): {
|
|
3
3
|
getSchema: () => Promise<Record<string, {
|
|
4
4
|
displayName: string;
|
|
@@ -31,7 +31,11 @@ export declare function createStorage(dataDirectory: string): {
|
|
|
31
31
|
createdAt: string;
|
|
32
32
|
updatedAt: string;
|
|
33
33
|
}>;
|
|
34
|
-
|
|
34
|
+
checkItemExists: ({ type, id }: {
|
|
35
|
+
type: string;
|
|
36
|
+
id: string;
|
|
37
|
+
}) => Promise<boolean>;
|
|
38
|
+
updateItem: (item: EditorialUpdateDataItem) => Promise<{
|
|
35
39
|
[x: string]: unknown;
|
|
36
40
|
id: string;
|
|
37
41
|
isDraft: boolean;
|
package/dist/lib/storage.js
CHANGED
|
@@ -90,6 +90,10 @@ export function createStorage(dataDirectory) {
|
|
|
90
90
|
}
|
|
91
91
|
return true;
|
|
92
92
|
}
|
|
93
|
+
async function checkItemExists({ type, id }) {
|
|
94
|
+
const content = await getContent({ production: false });
|
|
95
|
+
return !!content[type]?.[id];
|
|
96
|
+
}
|
|
93
97
|
async function createItem(item) {
|
|
94
98
|
const content = await getContent({ production: false });
|
|
95
99
|
content[item.type] = content[item.type] ?? {};
|
|
@@ -100,6 +104,9 @@ export function createStorage(dataDirectory) {
|
|
|
100
104
|
return parsedItem;
|
|
101
105
|
}
|
|
102
106
|
async function updateItem(item) {
|
|
107
|
+
if (!item.id) {
|
|
108
|
+
throw new Error("Item ID is required for update.");
|
|
109
|
+
}
|
|
103
110
|
const content = await getContent({ production: false });
|
|
104
111
|
content[item.type] = content[item.type] ?? {};
|
|
105
112
|
const oldItem = content[item.type][item.id];
|
|
@@ -108,7 +115,17 @@ export function createStorage(dataDirectory) {
|
|
|
108
115
|
...item,
|
|
109
116
|
updatedAt: new Date().toISOString(),
|
|
110
117
|
});
|
|
111
|
-
|
|
118
|
+
// Handle ID change: if the ID has changed, we need to delete the old item and create a new one with the new ID
|
|
119
|
+
if (newItem.hasOwnProperty("newId")) {
|
|
120
|
+
delete content[item.type][item.id];
|
|
121
|
+
const newId = item.newId || item.id;
|
|
122
|
+
delete newItem.newId;
|
|
123
|
+
newItem.id = newId;
|
|
124
|
+
content[item.type][newId] = newItem;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
content[item.type][item.id] = newItem;
|
|
128
|
+
}
|
|
112
129
|
// TODO: Use superjson to safely encode different types.
|
|
113
130
|
await writeFileSafe(dataPath, JSON.stringify(content, null, 2));
|
|
114
131
|
return newItem;
|
|
@@ -126,6 +143,7 @@ export function createStorage(dataDirectory) {
|
|
|
126
143
|
getLocalisationMessages,
|
|
127
144
|
saveLocalisationMessages,
|
|
128
145
|
createItem,
|
|
146
|
+
checkItemExists,
|
|
129
147
|
updateItem,
|
|
130
148
|
deleteItem,
|
|
131
149
|
saveContent,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type EditorialData, type EditorialDataItem, type EditorialSchema } from "@isardsat/editorial-common";
|
|
2
|
+
import { type Storage } from "../storage.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves referenced fields in an item, replacing IDs with full objects.
|
|
5
|
+
* Recursively resolves nested references.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveReferences(item: EditorialDataItem, schema: EditorialSchema, itemType: string, content: EditorialData, origin: string, resolvedIds?: Set<string>): EditorialDataItem;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves all references in a collection.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveCollectionReferences(collection: Record<string, EditorialDataItem>, schema: EditorialSchema, itemType: string, content: EditorialData, origin: string): Record<string, EditorialDataItem>;
|
|
12
|
+
export declare function updateOrRemoveReferences(params: {
|
|
13
|
+
schema: any;
|
|
14
|
+
content: Record<string, Record<string, any>>;
|
|
15
|
+
storage: Storage;
|
|
16
|
+
targetType: string;
|
|
17
|
+
oldId: string;
|
|
18
|
+
newId?: string | null;
|
|
19
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { getChoicesReference, } from "@isardsat/editorial-common";
|
|
2
|
+
import {} from "../storage.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves uploaded file paths to full URLs for an item.
|
|
5
|
+
*/
|
|
6
|
+
function resolveFileUrls(item, schema, itemType, origin) {
|
|
7
|
+
const resolvedItem = { ...item };
|
|
8
|
+
const itemSchema = schema[itemType];
|
|
9
|
+
if (!itemSchema)
|
|
10
|
+
return resolvedItem;
|
|
11
|
+
for (const [key, value] of Object.entries(resolvedItem)) {
|
|
12
|
+
if (!itemSchema.fields[key]?.isUploadedFile)
|
|
13
|
+
continue;
|
|
14
|
+
if (typeof value !== "string")
|
|
15
|
+
continue;
|
|
16
|
+
if (value.startsWith("http"))
|
|
17
|
+
continue;
|
|
18
|
+
resolvedItem[key] = `${origin}/${value}`;
|
|
19
|
+
}
|
|
20
|
+
return resolvedItem;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolves referenced fields in an item, replacing IDs with full objects.
|
|
24
|
+
* Recursively resolves nested references.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveReferences(item, schema, itemType, content, origin, resolvedIds = new Set()) {
|
|
27
|
+
const itemIdentifier = `${itemType}:${item.id}`;
|
|
28
|
+
// Prevent circular references
|
|
29
|
+
if (resolvedIds.has(itemIdentifier)) {
|
|
30
|
+
return resolveFileUrls(item, schema, itemType, origin);
|
|
31
|
+
}
|
|
32
|
+
resolvedIds.add(itemIdentifier);
|
|
33
|
+
// First resolve file URLs for the current item
|
|
34
|
+
let resolvedItem = resolveFileUrls(item, schema, itemType, origin);
|
|
35
|
+
const itemSchema = schema[itemType];
|
|
36
|
+
if (!itemSchema)
|
|
37
|
+
return resolvedItem;
|
|
38
|
+
for (const [fieldKey, fieldConfig] of Object.entries(itemSchema.fields)) {
|
|
39
|
+
if (fieldConfig.type !== "select" && fieldConfig.type !== "multiselect") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const referencedType = getChoicesReference(fieldConfig.choicesFixed);
|
|
43
|
+
if (!referencedType)
|
|
44
|
+
continue;
|
|
45
|
+
const referencedCollection = content[referencedType];
|
|
46
|
+
if (!referencedCollection)
|
|
47
|
+
continue;
|
|
48
|
+
const fieldValue = item[fieldKey];
|
|
49
|
+
if (fieldConfig.type === "select" && typeof fieldValue === "string") {
|
|
50
|
+
// Single reference - replace ID with full object
|
|
51
|
+
const referencedItem = referencedCollection[fieldValue];
|
|
52
|
+
if (referencedItem) {
|
|
53
|
+
// Recursively resolve nested references
|
|
54
|
+
resolvedItem[fieldKey] = resolveReferences(referencedItem, schema, referencedType, content, origin, new Set(resolvedIds));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (fieldConfig.type === "multiselect" &&
|
|
58
|
+
Array.isArray(fieldValue)) {
|
|
59
|
+
// Multiple references - replace IDs with full objects
|
|
60
|
+
resolvedItem[fieldKey] = fieldValue
|
|
61
|
+
.map((id) => {
|
|
62
|
+
const referencedItem = referencedCollection[id];
|
|
63
|
+
if (!referencedItem)
|
|
64
|
+
return null;
|
|
65
|
+
// Recursively resolve nested references
|
|
66
|
+
return resolveReferences(referencedItem, schema, referencedType, content, origin, new Set(resolvedIds));
|
|
67
|
+
})
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return resolvedItem;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves all references in a collection.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveCollectionReferences(collection, schema, itemType, content, origin) {
|
|
77
|
+
const resolvedCollection = {};
|
|
78
|
+
for (const [itemKey, item] of Object.entries(collection)) {
|
|
79
|
+
resolvedCollection[itemKey] = resolveReferences(item, schema, itemType, content, origin);
|
|
80
|
+
}
|
|
81
|
+
return resolvedCollection;
|
|
82
|
+
}
|
|
83
|
+
export async function updateOrRemoveReferences(params) {
|
|
84
|
+
const { schema, content, storage, targetType, oldId, newId } = params;
|
|
85
|
+
for (const [containerType, containerSchema] of Object.entries(schema)) {
|
|
86
|
+
const fields = containerSchema.fields || {};
|
|
87
|
+
const collection = content[containerType] || {};
|
|
88
|
+
// Find fields in this type that reference the target type
|
|
89
|
+
const referenceFields = Object.entries(fields).filter(([, fieldConfig]) => {
|
|
90
|
+
if (fieldConfig.type !== "select" &&
|
|
91
|
+
fieldConfig.type !== "multiselect") {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return getChoicesReference(fieldConfig.choicesFixed) === targetType;
|
|
95
|
+
});
|
|
96
|
+
if (referenceFields.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
for (const [itemId, item] of Object.entries(collection)) {
|
|
99
|
+
let changed = false;
|
|
100
|
+
const patchedItem = { ...item };
|
|
101
|
+
for (const [fieldKey, fieldConfig] of referenceFields) {
|
|
102
|
+
if (fieldConfig.type === "select") {
|
|
103
|
+
if (patchedItem[fieldKey] === oldId) {
|
|
104
|
+
patchedItem[fieldKey] = newId ?? null; // rename or remove
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (fieldConfig.type === "multiselect") {
|
|
109
|
+
const value = patchedItem[fieldKey];
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
let next;
|
|
112
|
+
if (newId != null) {
|
|
113
|
+
next = value.map((v) => (v === oldId ? newId : v));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
next = value.filter((v) => v !== oldId); // remove
|
|
117
|
+
}
|
|
118
|
+
if (next.length !== value.length ||
|
|
119
|
+
next.some((v, i) => v !== value[i])) {
|
|
120
|
+
patchedItem[fieldKey] = next;
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (changed) {
|
|
127
|
+
await storage.updateItem({
|
|
128
|
+
...patchedItem,
|
|
129
|
+
type: containerType,
|
|
130
|
+
id: itemId,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {} from "@isardsat/editorial-common";
|
|
2
|
+
import pkg from "../../../package.json" with { type: "json" };
|
|
3
|
+
const CURRENT_VERSION = pkg.version;
|
|
4
|
+
let latestVersionCache = null;
|
|
5
|
+
let lastChecked = 0;
|
|
6
|
+
export async function getCurrentAndLatestVersion() {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
if (latestVersionCache && now - lastChecked < 60_000 * 10) {
|
|
9
|
+
// cache 10 min
|
|
10
|
+
return { current: CURRENT_VERSION, latest: latestVersionCache };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch("https://registry.npmjs.org/@isardsat/editorial-admin", { headers: { Accept: "application/vnd.npm.install-v1+json" } });
|
|
14
|
+
const data = (await res.json());
|
|
15
|
+
latestVersionCache = data["dist-tags"].latest;
|
|
16
|
+
lastChecked = now;
|
|
17
|
+
return { current: CURRENT_VERSION, latest: latestVersionCache };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error("Failed to fetch latest version from npm registry");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/routes/data.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
2
|
-
import { EditorialDataItemSchema, EditorialDataSchema, EditorialDiffResponseSchema, EditorialSchemaSchema, } from "@isardsat/editorial-common";
|
|
2
|
+
import { EditorialDataItemSchema, EditorialDataSchema, EditorialDiffResponseSchema, EditorialSchemaSchema, EditorialUpdateDataItemSchema, } from "@isardsat/editorial-common";
|
|
3
3
|
import { createCache } from "../lib/cache.js";
|
|
4
4
|
import { firebaseAuth } from "../lib/middleware/auth.js";
|
|
5
5
|
import { getChangedFields } from "../lib/utils/diff.js";
|
|
6
|
-
import { resolveCollectionReferences, resolveReferences, } from "../lib/utils/
|
|
6
|
+
import { resolveCollectionReferences, resolveReferences, updateOrRemoveReferences, } from "../lib/utils/references.js";
|
|
7
7
|
import { generateMetaSchema } from "../lib/utils/schema.js";
|
|
8
8
|
export function createDataRoutes(config, storage) {
|
|
9
9
|
const app = new OpenAPIHono();
|
|
@@ -294,7 +294,7 @@ export function createDataRoutes(config, storage) {
|
|
|
294
294
|
app.openapi(createRoute({
|
|
295
295
|
method: "put",
|
|
296
296
|
path: "/data/{itemType}/{id}",
|
|
297
|
-
summary: "Create
|
|
297
|
+
summary: "Create object data by type and id",
|
|
298
298
|
request: {
|
|
299
299
|
params: z.object({
|
|
300
300
|
itemType: z.string().openapi({
|
|
@@ -324,12 +324,22 @@ export function createDataRoutes(config, storage) {
|
|
|
324
324
|
},
|
|
325
325
|
description: "Create a new object",
|
|
326
326
|
},
|
|
327
|
+
409: {
|
|
328
|
+
description: "Item with this type and id already exists",
|
|
329
|
+
},
|
|
327
330
|
},
|
|
328
331
|
tags: ["Data"],
|
|
329
332
|
middleware: [firebaseAuth(config.firebase?.projectId || "")],
|
|
330
333
|
security: [{ bearerAuth: [] }],
|
|
331
334
|
}), async (c) => {
|
|
332
335
|
const itemAtts = await c.req.json();
|
|
336
|
+
const itemExists = await storage.checkItemExists({
|
|
337
|
+
type: itemAtts.type,
|
|
338
|
+
id: itemAtts.id,
|
|
339
|
+
});
|
|
340
|
+
if (itemExists) {
|
|
341
|
+
return c.json({ error: "Item with this type and id already exists" }, 409);
|
|
342
|
+
}
|
|
333
343
|
const newItem = await storage.createItem(itemAtts);
|
|
334
344
|
cache.invalidateContent();
|
|
335
345
|
return c.json(newItem);
|
|
@@ -352,7 +362,7 @@ export function createDataRoutes(config, storage) {
|
|
|
352
362
|
body: {
|
|
353
363
|
content: {
|
|
354
364
|
"application/json": {
|
|
355
|
-
schema:
|
|
365
|
+
schema: EditorialUpdateDataItemSchema,
|
|
356
366
|
},
|
|
357
367
|
},
|
|
358
368
|
required: true,
|
|
@@ -367,15 +377,51 @@ export function createDataRoutes(config, storage) {
|
|
|
367
377
|
},
|
|
368
378
|
description: "Update object",
|
|
369
379
|
},
|
|
380
|
+
409: {
|
|
381
|
+
description: "Item with this type and id already exists",
|
|
382
|
+
},
|
|
370
383
|
},
|
|
371
384
|
tags: ["Data"],
|
|
372
385
|
middleware: [firebaseAuth(config.firebase?.projectId || "")],
|
|
373
386
|
security: [{ bearerAuth: [] }],
|
|
374
387
|
}), async (c) => {
|
|
388
|
+
const { itemType, id } = c.req.valid("param");
|
|
375
389
|
const itemAtts = await c.req.json();
|
|
376
|
-
|
|
390
|
+
if (itemAtts.newId) {
|
|
391
|
+
const itemWithNewIdExists = await storage.checkItemExists({
|
|
392
|
+
type: itemAtts.type,
|
|
393
|
+
id: itemAtts.newId,
|
|
394
|
+
});
|
|
395
|
+
if (itemWithNewIdExists) {
|
|
396
|
+
return c.json({ error: "Item with this type and this new id already exists" }, 409);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const oldId = id;
|
|
400
|
+
const newId = itemAtts.newId || itemAtts.id || id;
|
|
401
|
+
const renamed = oldId !== newId;
|
|
402
|
+
// Update renamed item first
|
|
403
|
+
const updatedItem = await storage.updateItem({
|
|
404
|
+
...itemAtts,
|
|
405
|
+
type: itemType,
|
|
406
|
+
id: oldId,
|
|
407
|
+
});
|
|
408
|
+
// Repair references if needed
|
|
409
|
+
if (renamed) {
|
|
410
|
+
const [schema, content] = await Promise.all([
|
|
411
|
+
storage.getSchema(),
|
|
412
|
+
storage.getContent({ production: false }), // preview/content being edited
|
|
413
|
+
]);
|
|
414
|
+
await updateOrRemoveReferences({
|
|
415
|
+
schema,
|
|
416
|
+
content,
|
|
417
|
+
storage,
|
|
418
|
+
targetType: itemType,
|
|
419
|
+
oldId,
|
|
420
|
+
newId,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
377
423
|
cache.invalidateContent();
|
|
378
|
-
return c.json(
|
|
424
|
+
return c.json(updatedItem);
|
|
379
425
|
});
|
|
380
426
|
app.openapi(createRoute({
|
|
381
427
|
method: "delete",
|
|
@@ -409,6 +455,19 @@ export function createDataRoutes(config, storage) {
|
|
|
409
455
|
}), async (c) => {
|
|
410
456
|
const { itemType, id } = c.req.valid("param");
|
|
411
457
|
await storage.deleteItem({ type: itemType, id });
|
|
458
|
+
// Remove references to the deleted item
|
|
459
|
+
const [schema, content] = await Promise.all([
|
|
460
|
+
storage.getSchema(),
|
|
461
|
+
storage.getContent({ production: false }),
|
|
462
|
+
]);
|
|
463
|
+
await updateOrRemoveReferences({
|
|
464
|
+
schema,
|
|
465
|
+
content,
|
|
466
|
+
storage,
|
|
467
|
+
targetType: itemType,
|
|
468
|
+
oldId: id,
|
|
469
|
+
newId: null,
|
|
470
|
+
});
|
|
412
471
|
cache.invalidateContent();
|
|
413
472
|
return c.json(true, 200);
|
|
414
473
|
});
|
|
@@ -492,7 +551,7 @@ export function createDataRoutes(config, storage) {
|
|
|
492
551
|
if (singletonKey) {
|
|
493
552
|
const previewItem = previewCollection[singletonKey];
|
|
494
553
|
const productionItem = productionCollection[singletonKey];
|
|
495
|
-
if (previewItem && !productionItem) {
|
|
554
|
+
if (previewItem && !previewItem.isDraft && !productionItem) {
|
|
496
555
|
// Singleton exists in preview but not in production = added
|
|
497
556
|
result.singles[itemType] = {
|
|
498
557
|
status: "added",
|
|
@@ -508,7 +567,8 @@ export function createDataRoutes(config, storage) {
|
|
|
508
567
|
production: productionItem,
|
|
509
568
|
};
|
|
510
569
|
}
|
|
511
|
-
else if (
|
|
570
|
+
else if (productionItem &&
|
|
571
|
+
previewItem?.updatedAt !== productionItem?.updatedAt) {
|
|
512
572
|
// Singleton has different updatedAt = modified
|
|
513
573
|
const changedFields = getChangedFields(previewItem, productionItem);
|
|
514
574
|
if (changedFields.length === 0) {
|
|
@@ -533,7 +593,7 @@ export function createDataRoutes(config, storage) {
|
|
|
533
593
|
// Find added and modified items
|
|
534
594
|
for (const [id, previewItem] of Object.entries(previewCollection)) {
|
|
535
595
|
const productionItem = productionCollection[id];
|
|
536
|
-
if (!productionItem) {
|
|
596
|
+
if (!previewItem.isDraft && !productionItem) {
|
|
537
597
|
// Item exists in preview but not in production = added
|
|
538
598
|
added.push({
|
|
539
599
|
id,
|
|
@@ -541,7 +601,8 @@ export function createDataRoutes(config, storage) {
|
|
|
541
601
|
updatedAt: previewItem.updatedAt,
|
|
542
602
|
});
|
|
543
603
|
}
|
|
544
|
-
else if (
|
|
604
|
+
else if (productionItem &&
|
|
605
|
+
previewItem.updatedAt !== productionItem.updatedAt) {
|
|
545
606
|
// Item has different updatedAt = modified (not yet published)
|
|
546
607
|
const changedFields = getChangedFields(previewItem, productionItem);
|
|
547
608
|
if (changedFields.length === 0) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OpenAPIHono, z } from "@hono/zod-openapi";
|
|
2
|
+
import { EditorialVersionResponseSchema } from "@isardsat/editorial-common";
|
|
3
|
+
import { getCurrentAndLatestVersion } from "../lib/utils/version.js";
|
|
4
|
+
export function createVersionRoutes(hooks) {
|
|
5
|
+
const app = new OpenAPIHono();
|
|
6
|
+
app.openapi({
|
|
7
|
+
method: "get",
|
|
8
|
+
path: "/version",
|
|
9
|
+
summary: "Get Editorial version",
|
|
10
|
+
responses: {
|
|
11
|
+
200: {
|
|
12
|
+
content: {
|
|
13
|
+
"application/json": {
|
|
14
|
+
schema: EditorialVersionResponseSchema,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
description: "Editorial version",
|
|
18
|
+
},
|
|
19
|
+
500: {
|
|
20
|
+
content: {
|
|
21
|
+
"application/json": { schema: z.object({ error: z.string() }) },
|
|
22
|
+
},
|
|
23
|
+
description: "Server error",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
tags: ["Version"],
|
|
27
|
+
}, async (c) => {
|
|
28
|
+
try {
|
|
29
|
+
const { current, latest } = await getCurrentAndLatestVersion();
|
|
30
|
+
return c.json({ current, latest }, 200);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return c.json({ error: "Failed to fetch latest version from npm registry" }, 500);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
app.openapi({
|
|
37
|
+
method: "post",
|
|
38
|
+
path: "/version/upgrade",
|
|
39
|
+
summary: "Trigger Editorial upgrade hook",
|
|
40
|
+
description: "Triggers a placeholder hook to start upgrade flow. Replace hook implementation with real deployment/migration logic.",
|
|
41
|
+
responses: {
|
|
42
|
+
202: {
|
|
43
|
+
content: {
|
|
44
|
+
"application/json": {
|
|
45
|
+
schema: z.boolean(),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
description: "Upgrade request accepted",
|
|
49
|
+
},
|
|
50
|
+
500: {
|
|
51
|
+
content: {
|
|
52
|
+
"application/json": { schema: z.object({ error: z.string() }) },
|
|
53
|
+
},
|
|
54
|
+
description: "Server error",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
tags: ["Version"],
|
|
58
|
+
}, async (c) => {
|
|
59
|
+
try {
|
|
60
|
+
await hooks.onUpgrade();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error("Error executing onUpgrade script:", error);
|
|
64
|
+
return c.json({ error: "Failed upgrade Editorial version" }, 500);
|
|
65
|
+
}
|
|
66
|
+
return c.json(true, 202);
|
|
67
|
+
});
|
|
68
|
+
return app;
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isardsat/editorial-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"jose": "^6.1.3",
|
|
16
16
|
"yaml": "^2.8.1",
|
|
17
17
|
"zod": "^4.1.11",
|
|
18
|
-
"@isardsat/editorial-admin": "^6.
|
|
19
|
-
"@isardsat/editorial-common": "^6.
|
|
18
|
+
"@isardsat/editorial-admin": "^6.21.0",
|
|
19
|
+
"@isardsat/editorial-common": "^6.21.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@tsconfig/node22": "^22.0.0",
|