@isardsat/editorial-server 6.13.2 → 6.15.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/schema.d.ts +5 -1
- package/dist/lib/storage.d.ts +5 -1
- package/dist/lib/utils/meta-schema.d.ts +1 -0
- package/dist/lib/utils/meta-schema.js +1 -0
- package/dist/lib/utils/schema.d.ts +30 -0
- package/dist/lib/utils/schema.js +105 -0
- package/dist/routes/actions.js +6 -0
- package/dist/routes/admin.js +6 -4
- package/dist/routes/config.js +1 -0
- package/dist/routes/data.js +180 -6
- package/dist/routes/files.js +8 -0
- package/package.json +3 -3
package/dist/lib/schema.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ export declare function createSchema(configDirectory: string): Promise<Record<st
|
|
|
2
2
|
displayName: string;
|
|
3
3
|
fields: Record<string, {
|
|
4
4
|
[x: string]: unknown;
|
|
5
|
-
type: "string" | "number" | "boolean" | "url" | "date" | "datetime" | "markdown" | "color" | "select";
|
|
5
|
+
type: "string" | "number" | "boolean" | "url" | "date" | "datetime" | "markdown" | "color" | "select" | "multiselect";
|
|
6
6
|
displayName: string;
|
|
7
7
|
optional: boolean;
|
|
8
8
|
displayExtra?: string | undefined;
|
|
9
9
|
placeholder?: string | undefined;
|
|
10
10
|
showInSummary?: boolean | undefined;
|
|
11
|
+
options?: string[] | undefined;
|
|
12
|
+
maxSelectedOptions?: number | undefined;
|
|
13
|
+
minSelectedOptions?: number | undefined;
|
|
14
|
+
isUploadedFile?: boolean | undefined;
|
|
11
15
|
}>;
|
|
12
16
|
filterBy?: string | undefined;
|
|
13
17
|
singleton?: boolean | undefined;
|
package/dist/lib/storage.d.ts
CHANGED
|
@@ -4,12 +4,16 @@ export declare function createStorage(dataDirectory: string): {
|
|
|
4
4
|
displayName: string;
|
|
5
5
|
fields: Record<string, {
|
|
6
6
|
[x: string]: unknown;
|
|
7
|
-
type: "string" | "number" | "boolean" | "url" | "date" | "datetime" | "markdown" | "color" | "select";
|
|
7
|
+
type: "string" | "number" | "boolean" | "url" | "date" | "datetime" | "markdown" | "color" | "select" | "multiselect";
|
|
8
8
|
displayName: string;
|
|
9
9
|
optional: boolean;
|
|
10
10
|
displayExtra?: string | undefined;
|
|
11
11
|
placeholder?: string | undefined;
|
|
12
12
|
showInSummary?: boolean | undefined;
|
|
13
|
+
options?: string[] | undefined;
|
|
14
|
+
maxSelectedOptions?: number | undefined;
|
|
15
|
+
minSelectedOptions?: number | undefined;
|
|
16
|
+
isUploadedFile?: boolean | undefined;
|
|
13
17
|
}>;
|
|
14
18
|
filterBy?: string | undefined;
|
|
15
19
|
singleton?: boolean | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type GenerateMetaSchemaOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Fields that exist in the Editorial's schema but should not be validated by the schema.
|
|
4
|
+
* These fields will be allowed without causing validation errors.
|
|
5
|
+
*/
|
|
6
|
+
allowedExtraFields?: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function generateMetaSchema(options?: GenerateMetaSchemaOptions): {
|
|
9
|
+
$schema: string;
|
|
10
|
+
title: string;
|
|
11
|
+
type: string;
|
|
12
|
+
additionalProperties: {
|
|
13
|
+
$ref: string;
|
|
14
|
+
};
|
|
15
|
+
definitions: {
|
|
16
|
+
SchemaItem: {
|
|
17
|
+
type: string;
|
|
18
|
+
properties: Record<string, any>;
|
|
19
|
+
required: string[];
|
|
20
|
+
additionalProperties: boolean;
|
|
21
|
+
};
|
|
22
|
+
Field: {
|
|
23
|
+
type: string;
|
|
24
|
+
properties: Record<string, any>;
|
|
25
|
+
required: string[];
|
|
26
|
+
additionalProperties: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EditorialSchemaItemFieldSchema, EditorialSchemaItemFieldType, EditorialSchemaItemSchema, } from "@isardsat/editorial-common";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export function generateMetaSchema(options = {}) {
|
|
4
|
+
const { allowedExtraFields = [] } = options;
|
|
5
|
+
function generateDefinition(schema) {
|
|
6
|
+
const fieldShape = schema.shape;
|
|
7
|
+
const properties = {};
|
|
8
|
+
const required = [];
|
|
9
|
+
for (const key in fieldShape) {
|
|
10
|
+
const zodField = fieldShape[key];
|
|
11
|
+
// Handle the "type" field separately since it's an enum with specific options
|
|
12
|
+
if (key === "type") {
|
|
13
|
+
properties[key] = {
|
|
14
|
+
type: "string",
|
|
15
|
+
enum: EditorialSchemaItemFieldType.options,
|
|
16
|
+
};
|
|
17
|
+
required.push(key);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// Unwrap ALL wrappers to get the inner type
|
|
21
|
+
let innerField = zodField;
|
|
22
|
+
let isOptional = false;
|
|
23
|
+
let defaultValue = undefined;
|
|
24
|
+
// Loop to unwrap all layers (ZodDefault, ZodOptional, ZodNullable, etc.)
|
|
25
|
+
while (innerField) {
|
|
26
|
+
if (innerField instanceof z.ZodOptional) {
|
|
27
|
+
isOptional = true;
|
|
28
|
+
innerField = innerField.unwrap();
|
|
29
|
+
}
|
|
30
|
+
else if (innerField instanceof z.ZodDefault) {
|
|
31
|
+
isOptional = true; // Un camp amb default és efectivament opcional
|
|
32
|
+
defaultValue = innerField._def.defaultValue;
|
|
33
|
+
innerField = innerField._def.innerType;
|
|
34
|
+
}
|
|
35
|
+
else if (innerField instanceof z.ZodNullable) {
|
|
36
|
+
innerField = innerField.unwrap();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// No more wrappers, exit loop
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Check the actual base type
|
|
44
|
+
if (innerField instanceof z.ZodString) {
|
|
45
|
+
properties[key] = { type: "string" };
|
|
46
|
+
}
|
|
47
|
+
else if (innerField instanceof z.ZodBoolean) {
|
|
48
|
+
properties[key] = {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
...(defaultValue !== undefined && { default: defaultValue }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
else if (innerField instanceof z.ZodNumber) {
|
|
54
|
+
properties[key] = {
|
|
55
|
+
type: "number",
|
|
56
|
+
...(defaultValue !== undefined && { default: defaultValue }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
else if (innerField instanceof z.ZodArray) {
|
|
60
|
+
properties[key] = { type: "array", items: { type: "string" } };
|
|
61
|
+
}
|
|
62
|
+
else if (innerField instanceof z.ZodEnum) {
|
|
63
|
+
properties[key] = {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: innerField.options,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.warn(`Unknown Zod type for field "${key}"`);
|
|
70
|
+
properties[key] = {};
|
|
71
|
+
}
|
|
72
|
+
// Add to required if not optional
|
|
73
|
+
if (!isOptional) {
|
|
74
|
+
required.push(key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const field of allowedExtraFields) {
|
|
78
|
+
if (!(field in properties)) {
|
|
79
|
+
properties[field] = {}; // Allow any value
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties,
|
|
85
|
+
required,
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const fieldDefinition = generateDefinition(EditorialSchemaItemFieldSchema);
|
|
90
|
+
const schemaItemDefinition = generateDefinition(EditorialSchemaItemSchema);
|
|
91
|
+
schemaItemDefinition.properties.fields = {
|
|
92
|
+
type: "object",
|
|
93
|
+
additionalProperties: { $ref: "#/definitions/Field" },
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
97
|
+
title: "Editorial Schema",
|
|
98
|
+
type: "object",
|
|
99
|
+
additionalProperties: { $ref: "#/definitions/SchemaItem" },
|
|
100
|
+
definitions: {
|
|
101
|
+
SchemaItem: schemaItemDefinition,
|
|
102
|
+
Field: fieldDefinition,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/routes/actions.js
CHANGED
|
@@ -7,6 +7,7 @@ export function createActionRoutes(storage, hooks) {
|
|
|
7
7
|
app.openapi(createRoute({
|
|
8
8
|
method: "post",
|
|
9
9
|
path: "/publish",
|
|
10
|
+
summary: "Trigger the publishing process",
|
|
10
11
|
request: {
|
|
11
12
|
body: {
|
|
12
13
|
content: {
|
|
@@ -26,6 +27,7 @@ export function createActionRoutes(storage, hooks) {
|
|
|
26
27
|
description: "Trigger the publishing process",
|
|
27
28
|
},
|
|
28
29
|
},
|
|
30
|
+
tags: ["Actions"],
|
|
29
31
|
}),
|
|
30
32
|
// TODO: Don't async, let the promises run in the background.
|
|
31
33
|
async (c) => {
|
|
@@ -62,6 +64,7 @@ export function createActionRoutes(storage, hooks) {
|
|
|
62
64
|
app.openapi(createRoute({
|
|
63
65
|
method: "post",
|
|
64
66
|
path: "/pull",
|
|
67
|
+
summary: "Trigger the pull process",
|
|
65
68
|
request: {
|
|
66
69
|
body: {
|
|
67
70
|
content: {
|
|
@@ -76,6 +79,7 @@ export function createActionRoutes(storage, hooks) {
|
|
|
76
79
|
description: "Trigger the pull process",
|
|
77
80
|
},
|
|
78
81
|
},
|
|
82
|
+
tags: ["Actions"],
|
|
79
83
|
}), async (c) => {
|
|
80
84
|
const { author } = c.req.valid("json");
|
|
81
85
|
await hooks.onPull(author);
|
|
@@ -84,6 +88,7 @@ export function createActionRoutes(storage, hooks) {
|
|
|
84
88
|
app.openapi(createRoute({
|
|
85
89
|
method: "post",
|
|
86
90
|
path: "/push",
|
|
91
|
+
summary: "Trigger the push process",
|
|
87
92
|
request: {
|
|
88
93
|
body: {
|
|
89
94
|
content: {
|
|
@@ -98,6 +103,7 @@ export function createActionRoutes(storage, hooks) {
|
|
|
98
103
|
description: "Trigger the push process",
|
|
99
104
|
},
|
|
100
105
|
},
|
|
106
|
+
tags: ["Actions"],
|
|
101
107
|
}), async (c) => {
|
|
102
108
|
const { author } = c.req.valid("json");
|
|
103
109
|
await hooks.onPush(author);
|
package/dist/routes/admin.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
2
|
-
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { OpenAPIHono, z } from "@hono/zod-openapi";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { z } from "@hono/zod-openapi";
|
|
6
5
|
export function createAdminRoutes(config) {
|
|
7
6
|
const app = new OpenAPIHono();
|
|
8
7
|
// TODO: This is package manager dependent
|
|
@@ -20,7 +19,8 @@ export function createAdminRoutes(config) {
|
|
|
20
19
|
content: {
|
|
21
20
|
"application/json": {
|
|
22
21
|
schema: z.object({
|
|
23
|
-
firebase: z
|
|
22
|
+
firebase: z
|
|
23
|
+
.object({
|
|
24
24
|
apiKey: z.string(),
|
|
25
25
|
authDomain: z.string(),
|
|
26
26
|
databaseURL: z.string(),
|
|
@@ -28,13 +28,15 @@ export function createAdminRoutes(config) {
|
|
|
28
28
|
storageBucket: z.string(),
|
|
29
29
|
messagingSenderId: z.string(),
|
|
30
30
|
dbUsersPath: z.string(),
|
|
31
|
-
})
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
32
33
|
}),
|
|
33
34
|
},
|
|
34
35
|
},
|
|
35
36
|
description: "Firebase configuration for admin",
|
|
36
37
|
},
|
|
37
38
|
},
|
|
39
|
+
tags: ["Admin"],
|
|
38
40
|
}, (c) => {
|
|
39
41
|
return c.json({ firebase: config.firebase });
|
|
40
42
|
});
|
package/dist/routes/config.js
CHANGED
package/dist/routes/data.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
2
|
-
import { EditorialDataItemSchema, EditorialDataSchema, EditorialSchemaSchema, } from "@isardsat/editorial-common";
|
|
2
|
+
import { EditorialDataItemSchema, EditorialDataSchema, EditorialSchemaSchema, getOptionsReference, } from "@isardsat/editorial-common";
|
|
3
|
+
import { generateMetaSchema } from "../lib/utils/schema.js";
|
|
3
4
|
function createCache() {
|
|
4
5
|
let schemaCache = null;
|
|
5
6
|
const contentCache = new Map();
|
|
@@ -36,6 +37,86 @@ function createCache() {
|
|
|
36
37
|
},
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves uploaded file paths to full URLs for an item.
|
|
42
|
+
*/
|
|
43
|
+
function resolveFileUrls(item, schema, itemType, origin) {
|
|
44
|
+
const resolvedItem = { ...item };
|
|
45
|
+
const itemSchema = schema[itemType];
|
|
46
|
+
if (!itemSchema)
|
|
47
|
+
return resolvedItem;
|
|
48
|
+
for (const [key, value] of Object.entries(resolvedItem)) {
|
|
49
|
+
if (!itemSchema.fields[key]?.isUploadedFile)
|
|
50
|
+
continue;
|
|
51
|
+
if (typeof value !== "string")
|
|
52
|
+
continue;
|
|
53
|
+
if (value.startsWith("http"))
|
|
54
|
+
continue;
|
|
55
|
+
resolvedItem[key] = `${origin}/${value}`;
|
|
56
|
+
}
|
|
57
|
+
return resolvedItem;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves referenced fields in an item, replacing IDs with full objects.
|
|
61
|
+
* Recursively resolves nested references.
|
|
62
|
+
*/
|
|
63
|
+
function resolveReferences(item, schema, itemType, content, origin, resolvedIds = new Set()) {
|
|
64
|
+
const itemIdentifier = `${itemType}:${item.id}`;
|
|
65
|
+
// Prevent circular references
|
|
66
|
+
if (resolvedIds.has(itemIdentifier)) {
|
|
67
|
+
return resolveFileUrls(item, schema, itemType, origin);
|
|
68
|
+
}
|
|
69
|
+
resolvedIds.add(itemIdentifier);
|
|
70
|
+
// First resolve file URLs for the current item
|
|
71
|
+
let resolvedItem = resolveFileUrls(item, schema, itemType, origin);
|
|
72
|
+
const itemSchema = schema[itemType];
|
|
73
|
+
if (!itemSchema)
|
|
74
|
+
return resolvedItem;
|
|
75
|
+
for (const [fieldKey, fieldConfig] of Object.entries(itemSchema.fields)) {
|
|
76
|
+
if (fieldConfig.type !== "select" && fieldConfig.type !== "multiselect") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const referencedType = getOptionsReference(fieldConfig.options);
|
|
80
|
+
if (!referencedType)
|
|
81
|
+
continue;
|
|
82
|
+
const referencedCollection = content[referencedType];
|
|
83
|
+
if (!referencedCollection)
|
|
84
|
+
continue;
|
|
85
|
+
const fieldValue = item[fieldKey];
|
|
86
|
+
if (fieldConfig.type === "select" && typeof fieldValue === "string") {
|
|
87
|
+
// Single reference - replace ID with full object
|
|
88
|
+
const referencedItem = referencedCollection[fieldValue];
|
|
89
|
+
if (referencedItem) {
|
|
90
|
+
// Recursively resolve nested references
|
|
91
|
+
resolvedItem[fieldKey] = resolveReferences(referencedItem, schema, referencedType, content, origin, new Set(resolvedIds));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (fieldConfig.type === "multiselect" &&
|
|
95
|
+
Array.isArray(fieldValue)) {
|
|
96
|
+
// Multiple references - replace IDs with full objects
|
|
97
|
+
resolvedItem[fieldKey] = fieldValue
|
|
98
|
+
.map((id) => {
|
|
99
|
+
const referencedItem = referencedCollection[id];
|
|
100
|
+
if (!referencedItem)
|
|
101
|
+
return null;
|
|
102
|
+
// Recursively resolve nested references
|
|
103
|
+
return resolveReferences(referencedItem, schema, referencedType, content, origin, new Set(resolvedIds));
|
|
104
|
+
})
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return resolvedItem;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves all references in a collection.
|
|
112
|
+
*/
|
|
113
|
+
function resolveCollectionReferences(collection, schema, itemType, content, origin) {
|
|
114
|
+
const resolvedCollection = {};
|
|
115
|
+
for (const [itemKey, item] of Object.entries(collection)) {
|
|
116
|
+
resolvedCollection[itemKey] = resolveReferences(item, schema, itemType, content, origin);
|
|
117
|
+
}
|
|
118
|
+
return resolvedCollection;
|
|
119
|
+
}
|
|
39
120
|
export function createDataRoutes(config, storage) {
|
|
40
121
|
const app = new OpenAPIHono();
|
|
41
122
|
const cache = createCache();
|
|
@@ -43,6 +124,7 @@ export function createDataRoutes(config, storage) {
|
|
|
43
124
|
app.openapi(createRoute({
|
|
44
125
|
method: "get",
|
|
45
126
|
path: "/schema",
|
|
127
|
+
summary: "Get Editorial schema",
|
|
46
128
|
responses: {
|
|
47
129
|
200: {
|
|
48
130
|
content: {
|
|
@@ -53,6 +135,7 @@ export function createDataRoutes(config, storage) {
|
|
|
53
135
|
description: "Get Editorial schema",
|
|
54
136
|
},
|
|
55
137
|
},
|
|
138
|
+
tags: ["Schema"],
|
|
56
139
|
}), async (c) => {
|
|
57
140
|
const schema = await cache.getSchema(storage);
|
|
58
141
|
return c.json(schema);
|
|
@@ -60,6 +143,7 @@ export function createDataRoutes(config, storage) {
|
|
|
60
143
|
app.openapi(createRoute({
|
|
61
144
|
method: "get",
|
|
62
145
|
path: "/data",
|
|
146
|
+
summary: "Get all Editorial data",
|
|
63
147
|
request: {
|
|
64
148
|
query: z.object({
|
|
65
149
|
lang: z
|
|
@@ -70,6 +154,13 @@ export function createDataRoutes(config, storage) {
|
|
|
70
154
|
example: "es_ES",
|
|
71
155
|
}),
|
|
72
156
|
preview: z.string().optional(),
|
|
157
|
+
resolve: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.openapi({
|
|
161
|
+
param: { name: "resolve", in: "query" },
|
|
162
|
+
description: "Resolve referenced fields to full objects",
|
|
163
|
+
}),
|
|
73
164
|
}),
|
|
74
165
|
},
|
|
75
166
|
responses: {
|
|
@@ -82,14 +173,26 @@ export function createDataRoutes(config, storage) {
|
|
|
82
173
|
description: "Get all Editorial data",
|
|
83
174
|
},
|
|
84
175
|
},
|
|
176
|
+
tags: ["Data"],
|
|
85
177
|
}), async (c) => {
|
|
86
|
-
const { preview } = c.req.valid("query");
|
|
178
|
+
const { preview, resolve } = c.req.valid("query");
|
|
87
179
|
const content = await cache.getContent(storage, { production: !preview });
|
|
88
|
-
|
|
180
|
+
if (!resolve) {
|
|
181
|
+
return c.json(content);
|
|
182
|
+
}
|
|
183
|
+
const origin = preview ? new URL(c.req.url).origin : publicFilesUrl;
|
|
184
|
+
// Resolve references for all collections
|
|
185
|
+
const schema = await cache.getSchema(storage);
|
|
186
|
+
const resolvedContent = {};
|
|
187
|
+
for (const [itemType, collection] of Object.entries(content)) {
|
|
188
|
+
resolvedContent[itemType] = resolveCollectionReferences(collection, schema, itemType, content, origin);
|
|
189
|
+
}
|
|
190
|
+
return c.json(resolvedContent);
|
|
89
191
|
});
|
|
90
192
|
app.openapi(createRoute({
|
|
91
193
|
method: "get",
|
|
92
194
|
path: "/data/{itemType}",
|
|
195
|
+
summary: "Get objects data by type",
|
|
93
196
|
request: {
|
|
94
197
|
params: z.object({
|
|
95
198
|
itemType: z.string().openapi({
|
|
@@ -106,6 +209,13 @@ export function createDataRoutes(config, storage) {
|
|
|
106
209
|
example: "es_ES",
|
|
107
210
|
}),
|
|
108
211
|
preview: z.string().optional(),
|
|
212
|
+
resolve: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.openapi({
|
|
216
|
+
param: { name: "resolve", in: "query" },
|
|
217
|
+
description: "Resolve referenced fields to full objects",
|
|
218
|
+
}),
|
|
109
219
|
}),
|
|
110
220
|
},
|
|
111
221
|
responses: {
|
|
@@ -121,9 +231,10 @@ export function createDataRoutes(config, storage) {
|
|
|
121
231
|
description: "Collection not found",
|
|
122
232
|
},
|
|
123
233
|
},
|
|
234
|
+
tags: ["Data"],
|
|
124
235
|
}), async (c) => {
|
|
125
236
|
const { itemType } = c.req.valid("param");
|
|
126
|
-
const { lang, preview } = c.req.valid("query");
|
|
237
|
+
const { lang, preview, resolve } = c.req.valid("query");
|
|
127
238
|
const origin = preview ? new URL(c.req.url).origin : publicFilesUrl;
|
|
128
239
|
const content = await cache.getContent(storage, {
|
|
129
240
|
production: !preview,
|
|
@@ -149,6 +260,11 @@ export function createDataRoutes(config, storage) {
|
|
|
149
260
|
}
|
|
150
261
|
}
|
|
151
262
|
}
|
|
263
|
+
// Resolve references if requested (includes file URL resolution)
|
|
264
|
+
if (resolve) {
|
|
265
|
+
return c.json(resolveCollectionReferences(collection, schema, itemType, content, origin));
|
|
266
|
+
}
|
|
267
|
+
// Apply file URL resolution for non-resolved requests
|
|
152
268
|
for (const [itemKey, itemValue] of Object.entries(collection)) {
|
|
153
269
|
for (const [key, value] of Object.entries(itemValue)) {
|
|
154
270
|
if (!schema[itemType].fields[key]?.isUploadedFile)
|
|
@@ -163,6 +279,7 @@ export function createDataRoutes(config, storage) {
|
|
|
163
279
|
app.openapi(createRoute({
|
|
164
280
|
method: "get",
|
|
165
281
|
path: "/data/{itemType}/ids",
|
|
282
|
+
summary: "Get object ids by type",
|
|
166
283
|
request: {
|
|
167
284
|
params: z.object({
|
|
168
285
|
itemType: z.string().openapi({
|
|
@@ -187,6 +304,7 @@ export function createDataRoutes(config, storage) {
|
|
|
187
304
|
description: "Item not found",
|
|
188
305
|
},
|
|
189
306
|
},
|
|
307
|
+
tags: ["Data"],
|
|
190
308
|
}), async (c) => {
|
|
191
309
|
const { itemType } = c.req.valid("param");
|
|
192
310
|
const { preview } = c.req.valid("query");
|
|
@@ -199,6 +317,7 @@ export function createDataRoutes(config, storage) {
|
|
|
199
317
|
app.openapi(createRoute({
|
|
200
318
|
method: "get",
|
|
201
319
|
path: "/data/{itemType}/{id}",
|
|
320
|
+
summary: "Get object data by type and id",
|
|
202
321
|
request: {
|
|
203
322
|
params: z.object({
|
|
204
323
|
itemType: z.string().openapi({
|
|
@@ -219,6 +338,13 @@ export function createDataRoutes(config, storage) {
|
|
|
219
338
|
example: "es_ES",
|
|
220
339
|
}),
|
|
221
340
|
preview: z.string().optional(),
|
|
341
|
+
resolve: z
|
|
342
|
+
.string()
|
|
343
|
+
.optional()
|
|
344
|
+
.openapi({
|
|
345
|
+
param: { name: "resolve", in: "query" },
|
|
346
|
+
description: "Resolve referenced fields to full objects",
|
|
347
|
+
}),
|
|
222
348
|
}),
|
|
223
349
|
},
|
|
224
350
|
responses: {
|
|
@@ -234,9 +360,10 @@ export function createDataRoutes(config, storage) {
|
|
|
234
360
|
description: "Collection or item not found",
|
|
235
361
|
},
|
|
236
362
|
},
|
|
363
|
+
tags: ["Data"],
|
|
237
364
|
}), async (c) => {
|
|
238
365
|
const { itemType, id } = c.req.valid("param");
|
|
239
|
-
const { lang, preview } = c.req.valid("query");
|
|
366
|
+
const { lang, preview, resolve } = c.req.valid("query");
|
|
240
367
|
const origin = preview ? new URL(c.req.url).origin : publicFilesUrl;
|
|
241
368
|
const content = await cache.getContent(storage, {
|
|
242
369
|
production: !preview,
|
|
@@ -247,7 +374,7 @@ export function createDataRoutes(config, storage) {
|
|
|
247
374
|
if (!collection) {
|
|
248
375
|
return c.notFound();
|
|
249
376
|
}
|
|
250
|
-
|
|
377
|
+
let item = collection[id];
|
|
251
378
|
if (!item) {
|
|
252
379
|
return c.notFound();
|
|
253
380
|
}
|
|
@@ -261,6 +388,12 @@ export function createDataRoutes(config, storage) {
|
|
|
261
388
|
}
|
|
262
389
|
}
|
|
263
390
|
}
|
|
391
|
+
// Resolve references if requested (includes file URL resolution)
|
|
392
|
+
if (resolve) {
|
|
393
|
+
item = resolveReferences(item, schema, itemType, content, origin);
|
|
394
|
+
return c.json(item);
|
|
395
|
+
}
|
|
396
|
+
// Apply file URL resolution for non-resolved requests
|
|
264
397
|
for (const [key, value] of Object.entries(item)) {
|
|
265
398
|
if (!schema[itemType].fields[key]?.isUploadedFile)
|
|
266
399
|
continue;
|
|
@@ -273,6 +406,7 @@ export function createDataRoutes(config, storage) {
|
|
|
273
406
|
app.openapi(createRoute({
|
|
274
407
|
method: "put",
|
|
275
408
|
path: "/data/{itemType}/{id}",
|
|
409
|
+
summary: "Create or update object data by type and id",
|
|
276
410
|
request: {
|
|
277
411
|
params: z.object({
|
|
278
412
|
itemType: z.string().openapi({
|
|
@@ -303,6 +437,7 @@ export function createDataRoutes(config, storage) {
|
|
|
303
437
|
description: "Create a new object",
|
|
304
438
|
},
|
|
305
439
|
},
|
|
440
|
+
tags: ["Data"],
|
|
306
441
|
}), async (c) => {
|
|
307
442
|
const itemAtts = await c.req.json();
|
|
308
443
|
const newItem = await storage.createItem(itemAtts);
|
|
@@ -312,6 +447,7 @@ export function createDataRoutes(config, storage) {
|
|
|
312
447
|
app.openapi(createRoute({
|
|
313
448
|
method: "patch",
|
|
314
449
|
path: "/data/{itemType}/{id}",
|
|
450
|
+
summary: "Update object data by type and id",
|
|
315
451
|
request: {
|
|
316
452
|
params: z.object({
|
|
317
453
|
itemType: z.string().openapi({
|
|
@@ -342,6 +478,7 @@ export function createDataRoutes(config, storage) {
|
|
|
342
478
|
description: "Update object",
|
|
343
479
|
},
|
|
344
480
|
},
|
|
481
|
+
tags: ["Data"],
|
|
345
482
|
}), async (c) => {
|
|
346
483
|
const itemAtts = await c.req.json();
|
|
347
484
|
const newItem = await storage.updateItem(itemAtts);
|
|
@@ -351,6 +488,7 @@ export function createDataRoutes(config, storage) {
|
|
|
351
488
|
app.openapi(createRoute({
|
|
352
489
|
method: "delete",
|
|
353
490
|
path: "/data/{itemType}/{id}",
|
|
491
|
+
summary: "Delete object by type and id",
|
|
354
492
|
request: {
|
|
355
493
|
params: z.object({
|
|
356
494
|
itemType: z.string().openapi({
|
|
@@ -373,11 +511,47 @@ export function createDataRoutes(config, storage) {
|
|
|
373
511
|
description: "Delete object",
|
|
374
512
|
},
|
|
375
513
|
},
|
|
514
|
+
tags: ["Data"],
|
|
376
515
|
}), async (c) => {
|
|
377
516
|
const { itemType, id } = c.req.valid("param");
|
|
378
517
|
await storage.deleteItem({ type: itemType, id });
|
|
379
518
|
cache.invalidateContent();
|
|
380
519
|
return c.json(true, 200);
|
|
381
520
|
});
|
|
521
|
+
app.openapi(createRoute({
|
|
522
|
+
method: "get",
|
|
523
|
+
path: "/meta-schema",
|
|
524
|
+
summary: "Get Editorial meta-schema",
|
|
525
|
+
request: {
|
|
526
|
+
query: z.object({
|
|
527
|
+
allowedExtraFields: z
|
|
528
|
+
.string()
|
|
529
|
+
.optional()
|
|
530
|
+
.openapi({
|
|
531
|
+
param: { name: "allowedExtraFields", in: "query" },
|
|
532
|
+
example: "customField,legacyField",
|
|
533
|
+
description: "Comma-separated list of extra field names that exist in the Editorial's schema but should not cause validation errors",
|
|
534
|
+
}),
|
|
535
|
+
}),
|
|
536
|
+
},
|
|
537
|
+
responses: {
|
|
538
|
+
200: {
|
|
539
|
+
content: {
|
|
540
|
+
"application/json": {
|
|
541
|
+
schema: z.record(z.string(), z.any()),
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
description: "JSON Schema meta-schema for the Editorial configuration",
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
tags: ["Schema"],
|
|
548
|
+
}), async (c) => {
|
|
549
|
+
const { allowedExtraFields: allowedExtraFieldsParam } = c.req.valid("query");
|
|
550
|
+
const allowedExtraFields = allowedExtraFieldsParam
|
|
551
|
+
? allowedExtraFieldsParam.split(",").map((f) => f.trim())
|
|
552
|
+
: [];
|
|
553
|
+
const metaSchema = generateMetaSchema({ allowedExtraFields });
|
|
554
|
+
return c.json(metaSchema);
|
|
555
|
+
});
|
|
382
556
|
return app;
|
|
383
557
|
}
|
package/dist/routes/files.js
CHANGED
|
@@ -14,6 +14,7 @@ export async function createFilesRoutes(config) {
|
|
|
14
14
|
app.openapi(createRoute({
|
|
15
15
|
method: "get",
|
|
16
16
|
path: "/files",
|
|
17
|
+
summary: "Get tree of public files with total size",
|
|
17
18
|
request: {
|
|
18
19
|
query: z.object({
|
|
19
20
|
preview: z.string().optional(),
|
|
@@ -29,6 +30,7 @@ export async function createFilesRoutes(config) {
|
|
|
29
30
|
description: "Get tree of public files with total size",
|
|
30
31
|
},
|
|
31
32
|
},
|
|
33
|
+
tags: ["Files"],
|
|
32
34
|
}),
|
|
33
35
|
// TODO: Index large files from bucket.
|
|
34
36
|
async (c) => {
|
|
@@ -109,6 +111,7 @@ export async function createFilesRoutes(config) {
|
|
|
109
111
|
app.openapi(createRoute({
|
|
110
112
|
method: "delete",
|
|
111
113
|
path: "/files",
|
|
114
|
+
summary: "Delete a file or directory (moved to deleted folder)",
|
|
112
115
|
request: {
|
|
113
116
|
body: {
|
|
114
117
|
content: {
|
|
@@ -146,6 +149,7 @@ export async function createFilesRoutes(config) {
|
|
|
146
149
|
description: "Server error",
|
|
147
150
|
},
|
|
148
151
|
},
|
|
152
|
+
tags: ["Files"],
|
|
149
153
|
}), async (c) => {
|
|
150
154
|
const { path: relativePathInput } = c.req.valid("json");
|
|
151
155
|
try {
|
|
@@ -174,6 +178,7 @@ export async function createFilesRoutes(config) {
|
|
|
174
178
|
app.openapi(createRoute({
|
|
175
179
|
method: "put",
|
|
176
180
|
path: "/files",
|
|
181
|
+
summary: "Upload files to a specified directory",
|
|
177
182
|
request: {
|
|
178
183
|
body: {
|
|
179
184
|
content: {
|
|
@@ -217,6 +222,7 @@ export async function createFilesRoutes(config) {
|
|
|
217
222
|
description: "Server error",
|
|
218
223
|
},
|
|
219
224
|
},
|
|
225
|
+
tags: ["Files"],
|
|
220
226
|
}), async (c) => {
|
|
221
227
|
try {
|
|
222
228
|
const body = await c.req.parseBody();
|
|
@@ -257,6 +263,7 @@ export async function createFilesRoutes(config) {
|
|
|
257
263
|
app.openapi(createRoute({
|
|
258
264
|
method: "post",
|
|
259
265
|
path: "/files/directory",
|
|
266
|
+
summary: "Create a new directory",
|
|
260
267
|
request: {
|
|
261
268
|
body: {
|
|
262
269
|
content: {
|
|
@@ -314,6 +321,7 @@ export async function createFilesRoutes(config) {
|
|
|
314
321
|
description: "Server error",
|
|
315
322
|
},
|
|
316
323
|
},
|
|
324
|
+
tags: ["Files"],
|
|
317
325
|
}), async (c) => {
|
|
318
326
|
try {
|
|
319
327
|
const { path, name } = c.req.valid("json");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isardsat/editorial-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.15.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.
|
|
18
|
-
"@isardsat/editorial-common": "^6.
|
|
17
|
+
"@isardsat/editorial-admin": "^6.15.0",
|
|
18
|
+
"@isardsat/editorial-common": "^6.15.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@tsconfig/node22": "^22.0.0",
|