@isardsat/editorial-server 6.10.0 → 6.11.1

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.
@@ -9,5 +9,6 @@ export declare function createSchema(configDirectory: string): Promise<Record<st
9
9
  placeholder?: string | undefined;
10
10
  showInSummary?: boolean | undefined;
11
11
  }>;
12
+ filterBy?: string | undefined;
12
13
  singleton?: boolean | undefined;
13
14
  }>>;
@@ -0,0 +1,46 @@
1
+ import type { EditorialData, EditorialDataObjectWithType } from "@isardsat/editorial-common";
2
+ export declare function createStorage(dataDirectory: string): {
3
+ getSchema: () => Promise<Record<string, {
4
+ displayName: string;
5
+ fields: Record<string, {
6
+ [x: string]: unknown;
7
+ type: "string" | "number" | "boolean" | "url" | "date" | "datetime" | "markdown" | "color" | "select";
8
+ displayName: string;
9
+ optional: boolean;
10
+ displayExtra?: string | undefined;
11
+ placeholder?: string | undefined;
12
+ showInSummary?: boolean | undefined;
13
+ }>;
14
+ singleton?: boolean | undefined;
15
+ }>>;
16
+ getContent: ({ production, }: {
17
+ production?: boolean;
18
+ }) => Promise<EditorialData>;
19
+ getLocalisationMessages: (langCode: string) => Promise<any>;
20
+ saveLocalisationMessages: (messages: any) => Promise<boolean>;
21
+ createItem: (item: EditorialDataObjectWithType) => Promise<{
22
+ [x: string]: unknown;
23
+ id: string;
24
+ isDraft: boolean;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }>;
28
+ updateItem: (item: EditorialDataObjectWithType) => Promise<{
29
+ [x: string]: unknown;
30
+ id: string;
31
+ isDraft: boolean;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }>;
35
+ deleteItem: (item: EditorialDataObjectWithType) => Promise<Record<string, Record<string, {
36
+ [x: string]: unknown;
37
+ id: string;
38
+ isDraft: boolean;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ }>>>;
42
+ saveContent: ({ production }: {
43
+ production?: boolean;
44
+ }) => Promise<boolean>;
45
+ };
46
+ export type Storage = ReturnType<typeof createStorage>;
@@ -0,0 +1,137 @@
1
+ import { EditorialDataItemSchema, EditorialDataSchema, EditorialSchemaSchema, } from "@isardsat/editorial-common";
2
+ import { readFile, readdir } from "fs/promises";
3
+ import { join } from "path";
4
+ import { parse } from "yaml";
5
+ import { writeFileSafe } from "./utils/fs.js";
6
+ export function createStorage(dataDirectory) {
7
+ const schemaPath = join(dataDirectory, "schema.yaml");
8
+ const dataPath = join(dataDirectory, "data.json");
9
+ const dataProdPath = join(dataDirectory, "data.prod.json");
10
+ const dataExtractedPath = join(dataDirectory, "data.messages.json");
11
+ async function getSchema() {
12
+ const schemaFile = await readFile(schemaPath, "utf-8").then((value) => parse(value));
13
+ const schema = EditorialSchemaSchema.parse(schemaFile);
14
+ return schema;
15
+ }
16
+ /**
17
+ * TODO: This should ideally cache the result of reading the file until an update occurs.
18
+ */
19
+ async function getContent({ production, }) {
20
+ const content = await readFile(production ? dataProdPath : dataPath, "utf-8").then((value) => EditorialDataSchema.parse(JSON.parse(value)));
21
+ return content;
22
+ }
23
+ async function getLocalisationMessages(langCode) {
24
+ return await readFile(join(dataDirectory, "locales", "messages", `${langCode}.json`), "utf-8").then((value) => JSON.parse(value));
25
+ }
26
+ async function saveContent({ production }) {
27
+ const content = await getContent({ production: false });
28
+ /** Do not save any items that have `isDraft` */
29
+ if (production) {
30
+ const filteredContent = {};
31
+ for (const [itemType, items] of Object.entries(content)) {
32
+ filteredContent[itemType] = {};
33
+ const typedItems = items;
34
+ for (const [itemId, item] of Object.entries(typedItems)) {
35
+ if (!item.isDraft) {
36
+ filteredContent[itemType][itemId] = item;
37
+ }
38
+ }
39
+ }
40
+ await writeFileSafe(dataProdPath, JSON.stringify(EditorialDataSchema.parse(filteredContent), null, 2));
41
+ const usedKeys = collectUsedMessageKeys(filteredContent);
42
+ console.log("🚀 ~ saveContent ~ usedKeys:", usedKeys);
43
+ const localesDir = join(dataDirectory, "locales", "messages");
44
+ const locales = await getAllLocalesMessages(localesDir);
45
+ for (const locale of locales) {
46
+ const pruned = pruneMessages(locale.messages, usedKeys);
47
+ await writeFileSafe(locale.path, JSON.stringify(pruned, null, 2));
48
+ }
49
+ return true;
50
+ }
51
+ await writeFileSafe(dataPath, JSON.stringify(EditorialDataSchema.parse(content), null, 2));
52
+ return true;
53
+ }
54
+ async function saveLocalisationMessages(messages) {
55
+ await writeFileSafe(dataExtractedPath, JSON.stringify(messages, null, 2));
56
+ return true;
57
+ }
58
+ async function createItem(item) {
59
+ const content = await getContent({ production: false });
60
+ content[item.type] = content[item.type] ?? {};
61
+ const parsedItem = EditorialDataItemSchema.parse(item);
62
+ content[item.type][item.id] = parsedItem;
63
+ // TODO: Use superjson to safely encode different types.
64
+ await writeFileSafe(dataPath, JSON.stringify(content, null, 2));
65
+ return parsedItem;
66
+ }
67
+ async function updateItem(item) {
68
+ const content = await getContent({ production: false });
69
+ content[item.type] = content[item.type] ?? {};
70
+ const oldItem = content[item.type][item.id];
71
+ const newItem = EditorialDataItemSchema.parse({
72
+ ...oldItem,
73
+ ...item,
74
+ updatedAt: new Date().toISOString(),
75
+ });
76
+ content[item.type][item.id] = newItem;
77
+ // TODO: Use superjson to safely encode different types.
78
+ await writeFileSafe(dataPath, JSON.stringify(content, null, 2));
79
+ return newItem;
80
+ }
81
+ async function deleteItem(item) {
82
+ const content = await getContent({ production: false });
83
+ delete content[item.type][item.id];
84
+ // TODO: Use superjson to safely encode different types.
85
+ await writeFileSafe(dataPath, JSON.stringify(content, null, 2));
86
+ return content;
87
+ }
88
+ function collectFromItem(value, keys) {
89
+ console.log("🚀 ~ collectFromItem ~ typeof value:", typeof value);
90
+ if (!value)
91
+ return;
92
+ if (typeof value === "object") {
93
+ if (typeof value.i18nKey === "string") {
94
+ keys.add(value.i18nKey);
95
+ }
96
+ for (const v of Object.values(value)) {
97
+ collectFromItem(v, keys);
98
+ }
99
+ }
100
+ }
101
+ function collectUsedMessageKeys(content) {
102
+ const keys = new Set();
103
+ for (const items of Object.values(content)) {
104
+ for (const item of Object.values(items)) {
105
+ collectFromItem(item, keys);
106
+ }
107
+ }
108
+ return keys;
109
+ }
110
+ async function getAllLocalesMessages(dir) {
111
+ const files = await readdir(dir);
112
+ return Promise.all(files.map(async (file) => ({
113
+ file,
114
+ path: join(dir, file),
115
+ messages: JSON.parse(await readFile(join(dir, file), "utf-8")),
116
+ })));
117
+ }
118
+ function pruneMessages(messages, usedKeys) {
119
+ const pruned = {};
120
+ for (const key of usedKeys) {
121
+ if (messages[key]) {
122
+ pruned[key] = messages[key];
123
+ }
124
+ }
125
+ return pruned;
126
+ }
127
+ return {
128
+ getSchema,
129
+ getContent,
130
+ getLocalisationMessages,
131
+ saveLocalisationMessages,
132
+ createItem,
133
+ updateItem,
134
+ deleteItem,
135
+ saveContent,
136
+ };
137
+ }
@@ -11,13 +11,14 @@ export declare function createStorage(dataDirectory: string): {
11
11
  placeholder?: string | undefined;
12
12
  showInSummary?: boolean | undefined;
13
13
  }>;
14
+ filterBy?: string | undefined;
14
15
  singleton?: boolean | undefined;
15
16
  }>>;
16
17
  getContent: ({ production, }: {
17
18
  production?: boolean;
18
19
  }) => Promise<EditorialData>;
19
20
  getLocalisationMessages: (langCode: string) => Promise<any>;
20
- saveLocalisationMessages: (messages: any) => Promise<boolean>;
21
+ saveLocalisationMessages: (newMessages: any) => Promise<boolean>;
21
22
  createItem: (item: EditorialDataObjectWithType) => Promise<{
22
23
  [x: string]: unknown;
23
24
  id: string;
@@ -1,5 +1,5 @@
1
1
  import { EditorialDataItemSchema, EditorialDataSchema, EditorialSchemaSchema, } from "@isardsat/editorial-common";
2
- import { readFile } from "fs/promises";
2
+ import { readFile, readdir } from "fs/promises";
3
3
  import { join } from "path";
4
4
  import { parse } from "yaml";
5
5
  import { writeFileSafe } from "./utils/fs.js";
@@ -8,6 +8,7 @@ export function createStorage(dataDirectory) {
8
8
  const dataPath = join(dataDirectory, "data.json");
9
9
  const dataProdPath = join(dataDirectory, "data.prod.json");
10
10
  const dataExtractedPath = join(dataDirectory, "data.messages.json");
11
+ const localesPath = join(dataDirectory, "locales", "messages");
11
12
  async function getSchema() {
12
13
  const schemaFile = await readFile(schemaPath, "utf-8").then((value) => parse(value));
13
14
  const schema = EditorialSchemaSchema.parse(schemaFile);
@@ -43,8 +44,50 @@ export function createStorage(dataDirectory) {
43
44
  await writeFileSafe(dataPath, JSON.stringify(EditorialDataSchema.parse(content), null, 2));
44
45
  return true;
45
46
  }
46
- async function saveLocalisationMessages(messages) {
47
- await writeFileSafe(dataExtractedPath, JSON.stringify(messages, null, 2));
47
+ async function getAllLocalesMessages(dir) {
48
+ const files = await readdir(dir);
49
+ return Promise.all(files.map(async (file) => ({
50
+ file,
51
+ path: join(dir, file),
52
+ messages: JSON.parse(await readFile(join(dir, file), "utf-8")),
53
+ })));
54
+ }
55
+ function pruneMessages(messages, deletedItemKeys) {
56
+ const pruned = { ...messages };
57
+ deletedItemKeys.forEach((key) => {
58
+ if (pruned.hasOwnProperty(key)) {
59
+ delete pruned[key];
60
+ }
61
+ });
62
+ return pruned;
63
+ }
64
+ async function saveLocalisationMessages(newMessages) {
65
+ const productionMessages = await readFile(dataExtractedPath, "utf-8").then((value) => JSON.parse(value));
66
+ await writeFileSafe(dataExtractedPath, JSON.stringify(newMessages, null, 2));
67
+ /**
68
+ * Get deleted items by checking
69
+ * keys that are in productionMessages but not in newMessages
70
+ */
71
+ const deletedItemKeys = [];
72
+ for (const itemType of Object.keys(productionMessages)) {
73
+ if (!newMessages[itemType]) {
74
+ /**
75
+ * We need to store both the full itemType with hash and the shortItemType without hash
76
+ * The itemType is used for locale files starting with underscore _
77
+ * The shortItemType is used for normal locale files
78
+ */
79
+ deletedItemKeys.push(itemType);
80
+ const shortItemType = itemType.split(".").slice(0, -1).join(".");
81
+ deletedItemKeys.push(shortItemType);
82
+ }
83
+ }
84
+ if (deletedItemKeys.length === 0)
85
+ return true;
86
+ const locales = await getAllLocalesMessages(localesPath);
87
+ for (const locale of locales) {
88
+ const pruned = pruneMessages(locale.messages, deletedItemKeys);
89
+ await writeFileSafe(locale.path, JSON.stringify(pruned, null, 2));
90
+ }
48
91
  return true;
49
92
  }
50
93
  async function createItem(item) {
@@ -143,6 +143,8 @@ export function createDataRoutes(config, storage) {
143
143
  for (const [key, message] of Object.entries(messages)) {
144
144
  const [contentKey, typeKey, fieldKey] = key.split(".");
145
145
  if (contentKey === itemType) {
146
+ if (!collection[typeKey])
147
+ continue;
146
148
  collection[typeKey][fieldKey] = message.defaultMessage;
147
149
  }
148
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isardsat/editorial-server",
3
- "version": "6.10.0",
3
+ "version": "6.11.1",
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.10.0",
18
- "@isardsat/editorial-common": "^6.10.0"
17
+ "@isardsat/editorial-admin": "^6.11.1",
18
+ "@isardsat/editorial-common": "^6.11.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@tsconfig/node22": "^22.0.0",
@@ -1,7 +0,0 @@
1
- import { type Logger } from "pino";
2
- export interface LoggerConfig {
3
- level?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
4
- pretty?: boolean;
5
- name?: string;
6
- }
7
- export declare function createLogger(config?: LoggerConfig): Logger;
@@ -1,26 +0,0 @@
1
- import pino, {} from "pino";
2
- export function createLogger(config = {}) {
3
- const isDevelopment = process.env.NODE_ENV !== "production";
4
- const level = config.level ?? (isDevelopment ? "debug" : "info");
5
- const pretty = config.pretty ?? isDevelopment;
6
- const transport = pretty
7
- ? {
8
- target: "pino-pretty",
9
- options: {
10
- colorize: true,
11
- translateTime: "HH:MM:ss Z",
12
- ignore: "pid,hostname",
13
- },
14
- }
15
- : undefined;
16
- return pino({
17
- name: config.name ?? "editorial-server",
18
- level,
19
- transport,
20
- serializers: {
21
- err: pino.stdSerializers.err,
22
- req: pino.stdSerializers.req,
23
- res: pino.stdSerializers.res,
24
- },
25
- });
26
- }