@isardsat/editorial-server 6.3.3 → 6.4.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/app.js +5 -4
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/schema.d.ts +4 -0
- package/dist/lib/storage.js +1 -0
- package/dist/routes/data.d.ts +2 -1
- package/dist/routes/data.js +41 -6
- package/dist/routes/files.d.ts +2 -1
- package/dist/routes/files.js +142 -10
- package/package.json +3 -3
package/dist/app.js
CHANGED
|
@@ -23,8 +23,8 @@ export async function createEditorialServer({ configDirectory = BASE_EDITORIAL_P
|
|
|
23
23
|
origin: "*",
|
|
24
24
|
}));
|
|
25
25
|
app.route("/api/v1", createConfigRoutes(config));
|
|
26
|
-
app.route("/api/v1", createDataRoutes(storage));
|
|
27
|
-
app.route("/api/v1", createFilesRoutes());
|
|
26
|
+
app.route("/api/v1", createDataRoutes(config, storage));
|
|
27
|
+
app.route("/api/v1", await createFilesRoutes(config));
|
|
28
28
|
app.route("/api/v1", createActionRoutes(storage, hooks));
|
|
29
29
|
app.route("/", createAdminRoutes(config));
|
|
30
30
|
app.doc("/doc", {
|
|
@@ -35,8 +35,9 @@ export async function createEditorialServer({ configDirectory = BASE_EDITORIAL_P
|
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
37
|
app.get("/doc/ui", swaggerUI({ url: "/doc" }));
|
|
38
|
-
app.use("
|
|
39
|
-
root:
|
|
38
|
+
app.use("/*", serveStatic({
|
|
39
|
+
root: config.publicDir,
|
|
40
|
+
rewriteRequestPath: (path) => path.replace(/^\/public/, ""),
|
|
40
41
|
}));
|
|
41
42
|
return {
|
|
42
43
|
app,
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export declare function createConfig(configDirectory: string): Promise<{
|
|
2
2
|
name: string;
|
|
3
3
|
publicUrl: string;
|
|
4
|
+
publicDir: string;
|
|
5
|
+
publicDeletedDir: string;
|
|
6
|
+
filesUrl: string;
|
|
7
|
+
largeFilesUrl: string;
|
|
4
8
|
previewUrl?: string | undefined;
|
|
5
9
|
silent?: boolean | undefined;
|
|
6
10
|
firebase?: {
|
package/dist/lib/schema.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export declare function createSchema(configDirectory: string): Promise<{
|
|
2
2
|
name: string;
|
|
3
3
|
publicUrl: string;
|
|
4
|
+
publicDir: string;
|
|
5
|
+
publicDeletedDir: string;
|
|
6
|
+
filesUrl: string;
|
|
7
|
+
largeFilesUrl: string;
|
|
4
8
|
previewUrl?: string | undefined;
|
|
5
9
|
silent?: boolean | undefined;
|
|
6
10
|
firebase?: {
|
package/dist/lib/storage.js
CHANGED
|
@@ -33,6 +33,7 @@ export function createStorage(dataDirectory) {
|
|
|
33
33
|
}
|
|
34
34
|
async function createItem(item) {
|
|
35
35
|
const content = await getContent();
|
|
36
|
+
content[item.type] = content[item.type] ?? {};
|
|
36
37
|
content[item.type][item.id] = EditorialDataItemSchema.parse(item);
|
|
37
38
|
// TODO: Use superjson to safely encode different types.
|
|
38
39
|
await writeFileSafe(dataPath, JSON.stringify(content, null, 2));
|
package/dist/routes/data.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { type EditorialConfig } from "@isardsat/editorial-common";
|
|
2
3
|
import type { Storage } from "../lib/storage.js";
|
|
3
|
-
export declare function createDataRoutes(storage: Storage): OpenAPIHono<import("hono").Env, {}, "/">;
|
|
4
|
+
export declare function createDataRoutes(config: EditorialConfig, storage: Storage): OpenAPIHono<import("hono").Env, {}, "/">;
|
package/dist/routes/data.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
2
2
|
import { EditorialDataItemSchema, EditorialDataSchema, EditorialSchemaSchema, } from "@isardsat/editorial-common";
|
|
3
|
-
export function createDataRoutes(storage) {
|
|
3
|
+
export function createDataRoutes(config, storage) {
|
|
4
4
|
const app = new OpenAPIHono();
|
|
5
|
+
const publicFilesUrl = config.filesUrl;
|
|
5
6
|
app.openapi(createRoute({
|
|
6
7
|
method: "get",
|
|
7
8
|
path: "/schema",
|
|
@@ -54,6 +55,7 @@ export function createDataRoutes(storage) {
|
|
|
54
55
|
param: { name: "lang", in: "query" },
|
|
55
56
|
example: "es_ES",
|
|
56
57
|
}),
|
|
58
|
+
preview: z.string().optional(),
|
|
57
59
|
}),
|
|
58
60
|
},
|
|
59
61
|
responses: {
|
|
@@ -65,22 +67,37 @@ export function createDataRoutes(storage) {
|
|
|
65
67
|
},
|
|
66
68
|
description: "Get objects by type",
|
|
67
69
|
},
|
|
70
|
+
404: {
|
|
71
|
+
description: "Collection not found",
|
|
72
|
+
},
|
|
68
73
|
},
|
|
69
74
|
}), async (c) => {
|
|
70
75
|
const { itemType } = c.req.valid("param");
|
|
71
|
-
const { lang } = c.req.valid("query");
|
|
76
|
+
const { lang, preview } = c.req.valid("query");
|
|
77
|
+
const origin = preview ? new URL(c.req.url).origin : publicFilesUrl;
|
|
72
78
|
const content = await storage.getContent();
|
|
79
|
+
const schema = await storage.getSchema();
|
|
73
80
|
const collection = content[itemType];
|
|
81
|
+
if (!collection) {
|
|
82
|
+
return c.notFound();
|
|
83
|
+
}
|
|
74
84
|
// TODO: Formalize this process.
|
|
75
85
|
if (lang) {
|
|
76
86
|
const messages = await storage.getLocalisationMessages(lang);
|
|
77
87
|
for (const [key, message] of Object.entries(messages)) {
|
|
78
|
-
const [contentKey, typeKey, fieldKey
|
|
88
|
+
const [contentKey, typeKey, fieldKey] = key.split(".");
|
|
79
89
|
if (contentKey === itemType) {
|
|
80
90
|
collection[typeKey][fieldKey] = message.defaultMessage;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
}
|
|
94
|
+
for (const [itemKey, itemValue] of Object.entries(collection)) {
|
|
95
|
+
for (const [key, value] of Object.entries(itemValue)) {
|
|
96
|
+
if (!schema[itemType].fields[key]?.isUploadedFile)
|
|
97
|
+
continue;
|
|
98
|
+
collection[itemKey][key] = `${origin}/${value}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
84
101
|
return c.json(collection);
|
|
85
102
|
});
|
|
86
103
|
app.openapi(createRoute({
|
|
@@ -131,6 +148,7 @@ export function createDataRoutes(storage) {
|
|
|
131
148
|
param: { name: "lang", in: "query" },
|
|
132
149
|
example: "es_ES",
|
|
133
150
|
}),
|
|
151
|
+
preview: z.string().optional(),
|
|
134
152
|
}),
|
|
135
153
|
},
|
|
136
154
|
responses: {
|
|
@@ -142,22 +160,39 @@ export function createDataRoutes(storage) {
|
|
|
142
160
|
},
|
|
143
161
|
description: "Get object data",
|
|
144
162
|
},
|
|
163
|
+
404: {
|
|
164
|
+
description: "Collection or item not found",
|
|
165
|
+
},
|
|
145
166
|
},
|
|
146
167
|
}), async (c) => {
|
|
147
168
|
const { itemType, id } = c.req.valid("param");
|
|
148
|
-
const { lang } = c.req.valid("query");
|
|
169
|
+
const { lang, preview } = c.req.valid("query");
|
|
170
|
+
const origin = preview ? new URL(c.req.url).origin : publicFilesUrl;
|
|
149
171
|
const content = await storage.getContent();
|
|
150
|
-
const
|
|
172
|
+
const schema = await storage.getSchema();
|
|
173
|
+
const collection = content[itemType];
|
|
174
|
+
if (!collection) {
|
|
175
|
+
return c.notFound();
|
|
176
|
+
}
|
|
177
|
+
const item = collection[id];
|
|
178
|
+
if (!item) {
|
|
179
|
+
return c.notFound();
|
|
180
|
+
}
|
|
151
181
|
// TODO: Formalize this process.
|
|
152
182
|
if (lang) {
|
|
153
183
|
const messages = await storage.getLocalisationMessages(lang);
|
|
154
184
|
for (const [key, message] of Object.entries(messages)) {
|
|
155
|
-
const [
|
|
185
|
+
const [, typeKey, fieldKey] = key.split(".");
|
|
156
186
|
if (typeKey === id) {
|
|
157
187
|
item[fieldKey] = message.defaultMessage;
|
|
158
188
|
}
|
|
159
189
|
}
|
|
160
190
|
}
|
|
191
|
+
for (const [key, value] of Object.entries(item)) {
|
|
192
|
+
if (!schema[itemType].fields[key]?.isUploadedFile)
|
|
193
|
+
continue;
|
|
194
|
+
item[key] = `${origin}/${value}`;
|
|
195
|
+
}
|
|
161
196
|
return c.json(item);
|
|
162
197
|
});
|
|
163
198
|
app.openapi(createRoute({
|
package/dist/routes/files.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
-
|
|
2
|
+
import { type EditorialConfig } from "@isardsat/editorial-common";
|
|
3
|
+
export declare function createFilesRoutes(config: EditorialConfig): Promise<OpenAPIHono<import("hono").Env, {}, "/">>;
|
package/dist/routes/files.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
2
2
|
import { EditorialFilesResponseSchema, } from "@isardsat/editorial-common";
|
|
3
3
|
import { readdirSync, statSync } from "node:fs";
|
|
4
|
-
import { access, constants, mkdir, rename } from "node:fs/promises";
|
|
5
|
-
import { basename, join, normalize } from "node:path";
|
|
6
|
-
export function createFilesRoutes() {
|
|
4
|
+
import { access, constants, mkdir, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, join, normalize, relative } from "node:path";
|
|
6
|
+
export async function createFilesRoutes(config) {
|
|
7
7
|
const app = new OpenAPIHono();
|
|
8
|
-
|
|
9
|
-
const
|
|
8
|
+
// Dynamically import the ES module and call its init function
|
|
9
|
+
const hookScript = join(process.cwd(), "editorial", "largeFileHandler");
|
|
10
|
+
const largeFilesHandler = await import(`${hookScript}.mjs`).then((mod) => mod.init());
|
|
11
|
+
const publicFilesUrl = config.filesUrl;
|
|
12
|
+
const publicDirPath = config.publicDir;
|
|
13
|
+
const deletedDirPath = config.publicDeletedDir;
|
|
10
14
|
app.openapi(createRoute({
|
|
11
15
|
method: "get",
|
|
12
16
|
path: "/files",
|
|
17
|
+
request: {
|
|
18
|
+
query: z.object({
|
|
19
|
+
preview: z.string().optional(),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
13
22
|
responses: {
|
|
14
23
|
200: {
|
|
15
24
|
content: {
|
|
@@ -20,7 +29,11 @@ export function createFilesRoutes() {
|
|
|
20
29
|
description: "Get tree of public files with total size",
|
|
21
30
|
},
|
|
22
31
|
},
|
|
23
|
-
}),
|
|
32
|
+
}),
|
|
33
|
+
// TODO: Index large files from bucket.
|
|
34
|
+
async (c) => {
|
|
35
|
+
const { preview } = c.req.valid("query");
|
|
36
|
+
const origin = preview ? new URL(c.req.url).origin : publicFilesUrl;
|
|
24
37
|
function calculateTotalSize(files) {
|
|
25
38
|
return files.reduce((total, file) => {
|
|
26
39
|
if (file.type === "file") {
|
|
@@ -33,15 +46,19 @@ export function createFilesRoutes() {
|
|
|
33
46
|
}, 0);
|
|
34
47
|
}
|
|
35
48
|
function readDirectoryChildren(path) {
|
|
49
|
+
// TODO: Gracefully handle missing directory?
|
|
36
50
|
const directory = readdirSync(path);
|
|
37
51
|
return directory
|
|
38
52
|
.filter((fileName) => !fileName.startsWith("."))
|
|
39
53
|
.map((fileName) => {
|
|
40
54
|
const file = statSync(join(path, fileName));
|
|
55
|
+
const filePath = join(path, fileName);
|
|
41
56
|
const isDirectory = file.isDirectory();
|
|
57
|
+
const relativePath = relative(publicDirPath, filePath);
|
|
42
58
|
return {
|
|
43
59
|
name: basename(fileName),
|
|
44
|
-
path:
|
|
60
|
+
path: isDirectory ? relativePath : `${origin}/${relativePath}`,
|
|
61
|
+
relativePath: relativePath,
|
|
45
62
|
size: file.size,
|
|
46
63
|
type: isDirectory ? "directory" : "file",
|
|
47
64
|
children: isDirectory
|
|
@@ -49,11 +66,45 @@ export function createFilesRoutes() {
|
|
|
49
66
|
: undefined,
|
|
50
67
|
};
|
|
51
68
|
})
|
|
52
|
-
.
|
|
69
|
+
.toSorted((a, b) => {
|
|
70
|
+
if (a.type === "directory" && b.type !== "directory")
|
|
71
|
+
return -1;
|
|
72
|
+
if (a.type !== "directory" && b.type === "directory")
|
|
73
|
+
return 1;
|
|
74
|
+
return a.name.localeCompare(b.name);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function mergeDirectoryTrees(localFiles, largeFiles) {
|
|
78
|
+
const merged = [...localFiles];
|
|
79
|
+
for (const largeFile of largeFiles) {
|
|
80
|
+
const existingIndex = merged.findIndex((file) => file.name === largeFile.name && file.type === "directory");
|
|
81
|
+
if (existingIndex !== -1 &&
|
|
82
|
+
merged[existingIndex].type === "directory") {
|
|
83
|
+
// Merge directories with same name
|
|
84
|
+
const existingDir = merged[existingIndex];
|
|
85
|
+
merged[existingIndex] = {
|
|
86
|
+
...existingDir,
|
|
87
|
+
children: mergeDirectoryTrees(existingDir.children || [], largeFile.children || []),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Add new file/directory
|
|
92
|
+
merged.push(largeFile);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return merged.toSorted((a, b) => {
|
|
96
|
+
if (a.type === "directory" && b.type !== "directory")
|
|
97
|
+
return -1;
|
|
98
|
+
if (a.type !== "directory" && b.type === "directory")
|
|
99
|
+
return 1;
|
|
100
|
+
return a.name.localeCompare(b.name);
|
|
101
|
+
});
|
|
53
102
|
}
|
|
54
103
|
const files = readDirectoryChildren(publicDirPath);
|
|
55
|
-
const
|
|
56
|
-
|
|
104
|
+
const largeFiles = await largeFilesHandler.list();
|
|
105
|
+
const mergedFiles = mergeDirectoryTrees(files, largeFiles);
|
|
106
|
+
const totalSize = calculateTotalSize(mergedFiles);
|
|
107
|
+
return c.json({ files: mergedFiles, totalSize });
|
|
57
108
|
});
|
|
58
109
|
app.openapi(createRoute({
|
|
59
110
|
method: "delete",
|
|
@@ -133,5 +184,86 @@ export function createFilesRoutes() {
|
|
|
133
184
|
return c.json({ error: "Server error" }, 500);
|
|
134
185
|
}
|
|
135
186
|
});
|
|
187
|
+
app.openapi(createRoute({
|
|
188
|
+
method: "put",
|
|
189
|
+
path: "/files",
|
|
190
|
+
request: {
|
|
191
|
+
body: {
|
|
192
|
+
content: {
|
|
193
|
+
"multipart/form-data": {
|
|
194
|
+
schema: z.object({
|
|
195
|
+
path: z.string().optional(),
|
|
196
|
+
files: z.any(),
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: true,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
responses: {
|
|
204
|
+
200: {
|
|
205
|
+
content: {
|
|
206
|
+
"application/json": {
|
|
207
|
+
schema: z.string().array(),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
description: "Files uploaded successfully",
|
|
211
|
+
},
|
|
212
|
+
400: {
|
|
213
|
+
content: {
|
|
214
|
+
"application/json": {
|
|
215
|
+
schema: z.object({
|
|
216
|
+
error: z.string(),
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
description: "Invalid request",
|
|
221
|
+
},
|
|
222
|
+
500: {
|
|
223
|
+
content: {
|
|
224
|
+
"application/json": {
|
|
225
|
+
schema: z.object({
|
|
226
|
+
error: z.string(),
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
description: "Server error",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}), async (c) => {
|
|
234
|
+
try {
|
|
235
|
+
const body = await c.req.parseBody();
|
|
236
|
+
const targetPath = body.path || "";
|
|
237
|
+
const files = body.files;
|
|
238
|
+
if (!files) {
|
|
239
|
+
return c.json({ error: "No files provided" }, 400);
|
|
240
|
+
}
|
|
241
|
+
const fileArray = Array.isArray(files) ? files : [files];
|
|
242
|
+
const uploadedFiles = [];
|
|
243
|
+
const targetDir = join(publicDirPath, targetPath);
|
|
244
|
+
const normalizedTargetDir = normalize(targetDir);
|
|
245
|
+
if (!normalizedTargetDir.startsWith(publicDirPath)) {
|
|
246
|
+
return c.json({ error: "Invalid target path" }, 400);
|
|
247
|
+
}
|
|
248
|
+
await mkdir(normalizedTargetDir, { recursive: true });
|
|
249
|
+
for (const file of fileArray) {
|
|
250
|
+
if (file instanceof File) {
|
|
251
|
+
if (file.size >= 1e6) {
|
|
252
|
+
return c.json({ error: "File too large" }, 400);
|
|
253
|
+
}
|
|
254
|
+
const fileName = file.name;
|
|
255
|
+
const filePath = join(normalizedTargetDir, fileName);
|
|
256
|
+
const buffer = await file.arrayBuffer();
|
|
257
|
+
await writeFile(filePath, new Uint8Array(buffer));
|
|
258
|
+
uploadedFiles.push(fileName);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return c.json(uploadedFiles, 200);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
console.error("Upload failed:", err);
|
|
265
|
+
return c.json({ error: "Server error" }, 500);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
136
268
|
return app;
|
|
137
269
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isardsat/editorial-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.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.6.20",
|
|
15
15
|
"yaml": "^2.7.0",
|
|
16
16
|
"zod": "^3.24.1",
|
|
17
|
-
"@isardsat/editorial-admin": "^6.
|
|
18
|
-
"@isardsat/editorial-common": "^6.
|
|
17
|
+
"@isardsat/editorial-admin": "^6.4.0",
|
|
18
|
+
"@isardsat/editorial-common": "^6.4.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@tsconfig/node22": "^22.0.0",
|