opacacms 0.3.18 → 0.3.19
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/{chunk-1bd7fz7n.js → chunk-8mqs2q7j.js} +1 -1
- package/dist/{chunk-2abqb0h6.js → chunk-9dsw6x4x.js} +23 -16
- package/dist/{chunk-b1g8jmth.js → chunk-mvz5hmdb.js} +263 -8
- package/dist/cli/index.js +3 -2
- package/dist/db/better-sqlite.js +1632 -42
- package/dist/db/bun-sqlite.js +1627 -37
- package/dist/db/d1.js +2326 -31
- package/dist/db/index.js +29 -4
- package/dist/db/postgres.js +1623 -32
- package/dist/db/sqlite.js +1631 -41
- package/dist/index.js +7 -9
- package/dist/runtimes/bun.js +3 -7
- package/dist/runtimes/cloudflare-workers.js +3068 -13
- package/dist/runtimes/next.js +3 -7
- package/dist/runtimes/node.js +3 -7
- package/dist/server.js +18 -13
- package/dist/storage/index.js +6 -3
- package/package.json +1 -1
- package/dist/chunk-40tky6qh.js +0 -10
- package/dist/chunk-5xpf5jxd.js +0 -114
- package/dist/chunk-gzbz5jwy.js +0 -700
- package/dist/chunk-h8v093av.js +0 -185
- package/dist/chunk-jq1drsen.js +0 -82
- package/dist/chunk-q5sb5dcr.js +0 -15
- package/dist/chunk-re459gm9.js +0 -429
- package/dist/chunk-s8mqwnm1.js +0 -14
- package/dist/chunk-z9ek88xr.js +0 -15
package/dist/db/sqlite.js
CHANGED
|
@@ -1,38 +1,1625 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
// src/db/kysely/field-mapper.ts
|
|
20
|
+
var exports_field_mapper = {};
|
|
21
|
+
__export(exports_field_mapper, {
|
|
22
|
+
toSnakeCase: () => toSnakeCase,
|
|
23
|
+
mapFieldToSQLiteType: () => mapFieldToSQLiteType,
|
|
24
|
+
mapFieldToPostgresType: () => mapFieldToPostgresType,
|
|
25
|
+
getRelationalFields: () => getRelationalFields,
|
|
26
|
+
flattenFields: () => flattenFields
|
|
27
|
+
});
|
|
28
|
+
function toSnakeCase(str) {
|
|
29
|
+
const res = str.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
30
|
+
if (res.startsWith("_") && !str.startsWith("_")) {
|
|
31
|
+
return res.slice(1);
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
}
|
|
35
|
+
function mapFieldToPostgresType(field) {
|
|
36
|
+
switch (field.type) {
|
|
37
|
+
case "text":
|
|
38
|
+
case "richtext":
|
|
39
|
+
case "select":
|
|
40
|
+
case "radio":
|
|
41
|
+
case "relationship":
|
|
42
|
+
return "text";
|
|
43
|
+
case "number":
|
|
44
|
+
return "double precision";
|
|
45
|
+
case "boolean":
|
|
46
|
+
return "boolean";
|
|
47
|
+
case "date":
|
|
48
|
+
return "timestamptz";
|
|
49
|
+
case "json":
|
|
50
|
+
case "file":
|
|
51
|
+
return "jsonb";
|
|
52
|
+
default:
|
|
53
|
+
return "text";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function mapFieldToSQLiteType(field) {
|
|
57
|
+
switch (field.type) {
|
|
58
|
+
case "text":
|
|
59
|
+
case "richtext":
|
|
60
|
+
case "select":
|
|
61
|
+
case "radio":
|
|
62
|
+
case "relationship":
|
|
63
|
+
case "date":
|
|
64
|
+
case "json":
|
|
65
|
+
case "file":
|
|
66
|
+
return "text";
|
|
67
|
+
case "number":
|
|
68
|
+
return "numeric";
|
|
69
|
+
case "boolean":
|
|
70
|
+
return "integer";
|
|
71
|
+
default:
|
|
72
|
+
return "text";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function flattenFields(fields, prefix = "") {
|
|
76
|
+
const result = [];
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
if (field.type === "join" || field.type === "virtual" || field.type === "ui")
|
|
79
|
+
continue;
|
|
80
|
+
const currentName = field.name ? `${prefix}${field.name}` : undefined;
|
|
81
|
+
if (field.type === "group") {
|
|
82
|
+
if (field.fields && Array.isArray(field.fields)) {
|
|
83
|
+
const nextPrefix = currentName ? `${currentName}__` : "";
|
|
84
|
+
result.push(...flattenFields(field.fields, nextPrefix));
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (field.type === "blocks") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (currentName) {
|
|
95
|
+
result.push({ ...field, name: currentName });
|
|
96
|
+
}
|
|
97
|
+
if (field.type === "row" || field.type === "collapsible") {
|
|
98
|
+
if (field.fields && Array.isArray(field.fields)) {
|
|
99
|
+
result.push(...flattenFields(field.fields, prefix));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
|
|
103
|
+
for (const tab of field.tabs) {
|
|
104
|
+
if (tab.fields && Array.isArray(tab.fields)) {
|
|
105
|
+
result.push(...flattenFields(tab.fields, prefix));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function getRelationalFields(fields, prefix = "") {
|
|
113
|
+
const result = [];
|
|
114
|
+
for (const field of fields) {
|
|
115
|
+
const currentName = field.name ? `${prefix}${field.name}` : undefined;
|
|
116
|
+
if (field.type === "relationship" && "hasMany" in field && field.hasMany || field.type === "blocks") {
|
|
117
|
+
if (currentName) {
|
|
118
|
+
result.push({ ...field, name: currentName });
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (field.type === "group" || field.type === "row" || field.type === "collapsible") {
|
|
123
|
+
if (field.fields && Array.isArray(field.fields)) {
|
|
124
|
+
const nextPrefix = field.type === "group" && field.name ? `${currentName}__` : prefix;
|
|
125
|
+
result.push(...getRelationalFields(field.fields, nextPrefix));
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
|
|
130
|
+
for (const tab of field.tabs) {
|
|
131
|
+
if (tab.fields && Array.isArray(tab.fields)) {
|
|
132
|
+
result.push(...getRelationalFields(tab.fields, prefix));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/db/system-schema.ts
|
|
141
|
+
var exports_system_schema = {};
|
|
142
|
+
__export(exports_system_schema, {
|
|
143
|
+
getSystemCollections: () => getSystemCollections
|
|
144
|
+
});
|
|
145
|
+
var getSystemCollections = () => [
|
|
146
|
+
{
|
|
147
|
+
slug: "_assets",
|
|
148
|
+
label: "Assets",
|
|
149
|
+
apiPath: "assets",
|
|
150
|
+
fields: [
|
|
151
|
+
{ name: "id", type: "text", required: true },
|
|
152
|
+
{ name: "key", type: "text", required: true },
|
|
153
|
+
{ name: "filename", type: "text", required: true },
|
|
154
|
+
{ name: "originalFilename", type: "text", required: true },
|
|
155
|
+
{ name: "mimeType", type: "text", required: true },
|
|
156
|
+
{ name: "filesize", type: "number", required: true },
|
|
157
|
+
{ name: "bucket", type: "text", required: true },
|
|
158
|
+
{ name: "folder", type: "text" },
|
|
159
|
+
{ name: "altText", type: "text" },
|
|
160
|
+
{ name: "caption", type: "text" },
|
|
161
|
+
{ name: "uploadedBy", type: "text" }
|
|
162
|
+
],
|
|
163
|
+
timestamps: true
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
slug: "_users",
|
|
167
|
+
apiPath: "users",
|
|
168
|
+
fields: [
|
|
169
|
+
{ name: "id", type: "text", required: true },
|
|
170
|
+
{ name: "name", type: "text", required: true },
|
|
171
|
+
{ name: "email", type: "text", required: true, unique: true },
|
|
172
|
+
{ name: "emailVerified", type: "boolean", required: true, defaultValue: false },
|
|
173
|
+
{ name: "image", type: "text" },
|
|
174
|
+
{ name: "role", type: "text" },
|
|
175
|
+
{ name: "banned", type: "boolean" },
|
|
176
|
+
{ name: "banReason", type: "text" },
|
|
177
|
+
{ name: "banExpires", type: "date" }
|
|
178
|
+
],
|
|
179
|
+
timestamps: true
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
slug: "_sessions",
|
|
183
|
+
fields: [
|
|
184
|
+
{ name: "id", type: "text", required: true },
|
|
185
|
+
{ name: "expiresAt", type: "date", required: true },
|
|
186
|
+
{ name: "token", type: "text", required: true, unique: true },
|
|
187
|
+
{ name: "ipAddress", type: "text" },
|
|
188
|
+
{ name: "userAgent", type: "text" },
|
|
189
|
+
{
|
|
190
|
+
name: "userId",
|
|
191
|
+
type: "text",
|
|
192
|
+
required: true,
|
|
193
|
+
references: { table: "_users", column: "id", onDelete: "cascade" }
|
|
194
|
+
},
|
|
195
|
+
{ name: "impersonatedBy", type: "text" }
|
|
196
|
+
],
|
|
197
|
+
timestamps: true,
|
|
198
|
+
hidden: true
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
slug: "_accounts",
|
|
202
|
+
fields: [
|
|
203
|
+
{ name: "id", type: "text", required: true },
|
|
204
|
+
{ name: "accountId", type: "text", required: true },
|
|
205
|
+
{ name: "providerId", type: "text", required: true },
|
|
206
|
+
{
|
|
207
|
+
name: "userId",
|
|
208
|
+
type: "text",
|
|
209
|
+
required: true,
|
|
210
|
+
references: { table: "_users", column: "id", onDelete: "cascade" }
|
|
211
|
+
},
|
|
212
|
+
{ name: "accessToken", type: "text" },
|
|
213
|
+
{ name: "refreshToken", type: "text" },
|
|
214
|
+
{ name: "idToken", type: "text" },
|
|
215
|
+
{ name: "accessTokenExpiresAt", type: "date" },
|
|
216
|
+
{ name: "refreshTokenExpiresAt", type: "date" },
|
|
217
|
+
{ name: "scope", type: "text" },
|
|
218
|
+
{ name: "password", type: "text" }
|
|
219
|
+
],
|
|
220
|
+
timestamps: true,
|
|
221
|
+
hidden: true
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
slug: "_verifications",
|
|
225
|
+
fields: [
|
|
226
|
+
{ name: "id", type: "text", required: true },
|
|
227
|
+
{ name: "identifier", type: "text", required: true },
|
|
228
|
+
{ name: "value", type: "text", required: true },
|
|
229
|
+
{ name: "expiresAt", type: "date", required: true }
|
|
230
|
+
],
|
|
231
|
+
timestamps: true,
|
|
232
|
+
hidden: true
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
slug: "_api_keys",
|
|
236
|
+
fields: [
|
|
237
|
+
{ name: "id", type: "text", required: true },
|
|
238
|
+
{ name: "configId", type: "text", required: true },
|
|
239
|
+
{ name: "name", type: "text" },
|
|
240
|
+
{ name: "start", type: "text" },
|
|
241
|
+
{ name: "prefix", type: "text" },
|
|
242
|
+
{ name: "key", type: "text", required: true },
|
|
243
|
+
{ name: "referenceId", type: "text", required: true },
|
|
244
|
+
{ name: "refillInterval", type: "number" },
|
|
245
|
+
{ name: "refillAmount", type: "number" },
|
|
246
|
+
{ name: "lastRefillAt", type: "date" },
|
|
247
|
+
{ name: "enabled", type: "boolean", required: true },
|
|
248
|
+
{ name: "rateLimitEnabled", type: "boolean", required: true },
|
|
249
|
+
{ name: "rateLimitTimeWindow", type: "number" },
|
|
250
|
+
{ name: "rateLimitMax", type: "number" },
|
|
251
|
+
{ name: "requestCount", type: "number", required: true },
|
|
252
|
+
{ name: "remaining", type: "number" },
|
|
253
|
+
{ name: "lastRequest", type: "date" },
|
|
254
|
+
{ name: "expiresAt", type: "date" },
|
|
255
|
+
{ name: "permissions", type: "text" },
|
|
256
|
+
{ name: "metadata", type: "text" }
|
|
257
|
+
],
|
|
258
|
+
timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" },
|
|
259
|
+
hidden: true
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
slug: "_plugin_settings",
|
|
263
|
+
label: "Plugin Settings",
|
|
264
|
+
apiPath: "plugin-settings",
|
|
265
|
+
fields: [
|
|
266
|
+
{ name: "pluginName", type: "text", required: true, unique: true },
|
|
267
|
+
{ name: "config", type: "json", required: true }
|
|
268
|
+
],
|
|
269
|
+
timestamps: true,
|
|
270
|
+
hidden: false,
|
|
271
|
+
admin: {
|
|
272
|
+
hidden: true,
|
|
273
|
+
disableAdmin: true
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
slug: "_audit_logs",
|
|
278
|
+
label: "Audit Logs",
|
|
279
|
+
apiPath: "audit-logs",
|
|
280
|
+
fields: [
|
|
281
|
+
{ name: "id", type: "text", required: true },
|
|
282
|
+
{ name: "operation", type: "text", required: true },
|
|
283
|
+
{ name: "collection", type: "text", required: true },
|
|
284
|
+
{ name: "entity_id", type: "text", required: true },
|
|
285
|
+
{ name: "user_id", type: "text" },
|
|
286
|
+
{ name: "previous_data", type: "json" },
|
|
287
|
+
{ name: "new_data", type: "json" },
|
|
288
|
+
{ name: "timestamp", type: "date" }
|
|
289
|
+
],
|
|
290
|
+
timestamps: true,
|
|
291
|
+
admin: {
|
|
292
|
+
hidden: true,
|
|
293
|
+
disableAdmin: true
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
slug: "_doc_versions",
|
|
298
|
+
label: "Document Versions",
|
|
299
|
+
apiPath: "doc-versions",
|
|
300
|
+
fields: [
|
|
301
|
+
{ name: "id", type: "text", required: true },
|
|
302
|
+
{ name: "collection", type: "text", required: true },
|
|
303
|
+
{ name: "entity_id", type: "text", required: true },
|
|
304
|
+
{ name: "data", type: "json", required: true },
|
|
305
|
+
{ name: "status", type: "text" },
|
|
306
|
+
{ name: "autosave", type: "boolean", defaultValue: false },
|
|
307
|
+
{ name: "version_name", type: "text" },
|
|
308
|
+
{ name: "created_by", type: "text" }
|
|
309
|
+
],
|
|
310
|
+
timestamps: true,
|
|
311
|
+
admin: {
|
|
312
|
+
hidden: true,
|
|
313
|
+
disableAdmin: true
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
// src/utils/context.ts
|
|
319
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
320
|
+
function getUserIdFromContext() {
|
|
321
|
+
return requestContext.getStore()?.userId || null;
|
|
322
|
+
}
|
|
323
|
+
function getPreviousDataFromContext() {
|
|
324
|
+
return requestContext.getStore()?.previousData || null;
|
|
325
|
+
}
|
|
326
|
+
var GLOBAL_CONTEXT_KEY, requestContext;
|
|
327
|
+
var init_context = __esm(() => {
|
|
328
|
+
GLOBAL_CONTEXT_KEY = Symbol.for("opacacms.requestContext");
|
|
329
|
+
if (!globalThis[GLOBAL_CONTEXT_KEY]) {
|
|
330
|
+
globalThis[GLOBAL_CONTEXT_KEY] = new AsyncLocalStorage;
|
|
331
|
+
}
|
|
332
|
+
requestContext = globalThis[GLOBAL_CONTEXT_KEY];
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// src/db/kysely/plugins/audit-logging.ts
|
|
336
|
+
class AuditLoggingPlugin {
|
|
337
|
+
auditTable;
|
|
338
|
+
getUserId;
|
|
339
|
+
db;
|
|
340
|
+
queryNodes = new WeakMap;
|
|
341
|
+
constructor(config = {}) {
|
|
342
|
+
this.auditTable = config.auditTable || "_audit_logs";
|
|
343
|
+
this.getUserId = config.getUserId;
|
|
344
|
+
}
|
|
345
|
+
setDb(db) {
|
|
346
|
+
this.db = db;
|
|
347
|
+
}
|
|
348
|
+
transformQuery(args) {
|
|
349
|
+
this.queryNodes.set(args.queryId, args.node);
|
|
350
|
+
return args.node;
|
|
351
|
+
}
|
|
352
|
+
async transformResult(args) {
|
|
353
|
+
const { result, queryId } = args;
|
|
354
|
+
const queryNode = this.queryNodes.get(queryId);
|
|
355
|
+
this.queryNodes.delete(queryId);
|
|
356
|
+
if (!queryNode || queryNode.kind === "SelectQueryNode")
|
|
357
|
+
return result;
|
|
358
|
+
let tableName = "";
|
|
359
|
+
let operation = "other";
|
|
360
|
+
if (queryNode.kind === "InsertQueryNode") {
|
|
361
|
+
tableName = this.getTableName(queryNode.into);
|
|
362
|
+
operation = "create";
|
|
363
|
+
} else if (queryNode.kind === "UpdateQueryNode") {
|
|
364
|
+
tableName = this.getTableName(queryNode.table);
|
|
365
|
+
operation = "update";
|
|
366
|
+
} else if (queryNode.kind === "DeleteQueryNode") {
|
|
367
|
+
tableName = this.getTableName(queryNode.table || queryNode.from?.froms?.[0]);
|
|
368
|
+
operation = "delete";
|
|
369
|
+
}
|
|
370
|
+
if (!tableName || tableName === this.auditTable)
|
|
371
|
+
return result;
|
|
372
|
+
const stashedPrevious = getPreviousDataFromContext();
|
|
373
|
+
this.logOperation(operation, tableName, queryNode, result, stashedPrevious).catch((err) => {
|
|
374
|
+
console.error("[OpacaCMS] Audit logging failed:", err);
|
|
375
|
+
});
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
getTableName(node) {
|
|
379
|
+
if (!node)
|
|
380
|
+
return "";
|
|
381
|
+
if (node.kind === "TableNode") {
|
|
382
|
+
return node.table?.identifier?.name || node.table?.name || "";
|
|
383
|
+
}
|
|
384
|
+
if (node.table?.kind === "TableNode") {
|
|
385
|
+
return node.table.table?.identifier?.name || node.table.table?.name || "";
|
|
386
|
+
}
|
|
387
|
+
return node.table?.name || node.table?.identifier?.name || node.name || "";
|
|
388
|
+
}
|
|
389
|
+
async logOperation(operation, collection, queryNode, result, stashedPrevious = null) {
|
|
390
|
+
if (!this.db) {
|
|
391
|
+
console.warn("[OpacaCMS] AuditLoggingPlugin: No database instance set, skipping log.");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
let entityId = "unknown";
|
|
396
|
+
let newData = null;
|
|
397
|
+
let previousData = null;
|
|
398
|
+
if (queryNode.kind === "InsertQueryNode") {
|
|
399
|
+
entityId = result.insertId?.toString() || "new";
|
|
400
|
+
const columns = queryNode.columns;
|
|
401
|
+
const valuesNode = queryNode.values;
|
|
402
|
+
if (Array.isArray(columns) && valuesNode?.kind === "ValuesNode") {
|
|
403
|
+
const row = valuesNode.values?.[0]?.values;
|
|
404
|
+
if (Array.isArray(row)) {
|
|
405
|
+
newData = {};
|
|
406
|
+
for (let i = 0;i < columns.length; i++) {
|
|
407
|
+
const col = columns[i]?.column?.name || columns[i]?.name;
|
|
408
|
+
if (col) {
|
|
409
|
+
const node = row[i];
|
|
410
|
+
newData[col] = node?.value !== undefined ? node.value : node?.name || node?.identifier?.name || null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if ((entityId === "new" || entityId === "unknown") && stashedPrevious?.id) {
|
|
416
|
+
entityId = stashedPrevious.id.toString();
|
|
417
|
+
}
|
|
418
|
+
} else if (queryNode.kind === "UpdateQueryNode") {
|
|
419
|
+
entityId = "updated";
|
|
420
|
+
const where = queryNode.where;
|
|
421
|
+
const filter = where?.where;
|
|
422
|
+
if (filter?.kind === "BinaryOperationNode") {
|
|
423
|
+
const op = filter;
|
|
424
|
+
const leftNode = op.left;
|
|
425
|
+
const leftName = leftNode?.column?.name || leftNode?.name || leftNode?.identifier?.name;
|
|
426
|
+
if (op.operator?.operator === "=" && leftName === "id") {
|
|
427
|
+
const right = op.right;
|
|
428
|
+
entityId = (right?.value !== undefined ? right.value : right?.name || right?.identifier?.name)?.toString() || "updated";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (entityId === "updated" && stashedPrevious?.id) {
|
|
432
|
+
entityId = stashedPrevious.id.toString();
|
|
433
|
+
}
|
|
434
|
+
const updates = queryNode.updates;
|
|
435
|
+
if (Array.isArray(updates)) {
|
|
436
|
+
newData = {};
|
|
437
|
+
for (const upd of updates) {
|
|
438
|
+
const col = upd.column?.column?.name || upd.column?.name;
|
|
439
|
+
if (col) {
|
|
440
|
+
const valNode = upd.value;
|
|
441
|
+
newData[col] = valNode?.value !== undefined ? valNode.value : valNode?.name || valNode?.identifier?.name || null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (entityId !== "updated" && entityId !== "unknown") {
|
|
446
|
+
previousData = stashedPrevious;
|
|
447
|
+
if (previousData) {
|
|
448
|
+
console.debug(`[AuditLoggingPlugin] Used stashed previous data for ${entityId}`);
|
|
449
|
+
}
|
|
450
|
+
if (!previousData) {
|
|
451
|
+
previousData = getPreviousDataFromContext();
|
|
452
|
+
}
|
|
453
|
+
if (!previousData) {
|
|
454
|
+
try {
|
|
455
|
+
previousData = await this.db.selectFrom(collection).selectAll().where("id", "=", entityId).executeTakeFirst();
|
|
456
|
+
if (previousData) {
|
|
457
|
+
console.debug(`[AuditLoggingPlugin] Fallback SELECT fetched data for ${entityId}`);
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} else if (queryNode.kind === "DeleteQueryNode") {
|
|
463
|
+
entityId = "deleted";
|
|
464
|
+
}
|
|
465
|
+
await this.db.insertInto(this.auditTable).values({
|
|
466
|
+
id: crypto.randomUUID(),
|
|
467
|
+
operation,
|
|
468
|
+
collection,
|
|
469
|
+
entity_id: entityId,
|
|
470
|
+
user_id: (this.getUserId ? this.getUserId() : null) || getUserIdFromContext(),
|
|
471
|
+
previous_data: previousData ? JSON.stringify(previousData) : null,
|
|
472
|
+
new_data: newData ? JSON.stringify(newData) : "{}",
|
|
473
|
+
timestamp: new Date().toISOString(),
|
|
474
|
+
created_at: new Date().toISOString(),
|
|
475
|
+
updated_at: new Date().toISOString()
|
|
476
|
+
}).execute();
|
|
477
|
+
} catch (err) {
|
|
478
|
+
console.error("[OpacaCMS] Failed to write audit log:", err);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
var init_audit_logging = __esm(() => {
|
|
483
|
+
init_context();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// src/db/kysely/plugins/auto-timestamps.ts
|
|
487
|
+
class AutoTimestampsPlugin {
|
|
488
|
+
createdAtColumn;
|
|
489
|
+
updatedAtColumn;
|
|
490
|
+
constructor(config = {}) {
|
|
491
|
+
this.createdAtColumn = config.createdAtColumn || "created_at";
|
|
492
|
+
this.updatedAtColumn = config.updatedAtColumn || "updated_at";
|
|
493
|
+
}
|
|
494
|
+
transformQuery(args) {
|
|
495
|
+
const { node } = args;
|
|
496
|
+
const now = new Date().toISOString();
|
|
497
|
+
if (node.kind === "InsertQueryNode") {} else if (node.kind === "UpdateQueryNode") {}
|
|
498
|
+
return node;
|
|
499
|
+
}
|
|
500
|
+
async transformResult(args) {
|
|
501
|
+
return args.result;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/db/kysely/plugins/cursor-pagination.ts
|
|
506
|
+
class CursorPaginationPlugin {
|
|
507
|
+
after;
|
|
508
|
+
cursorColumn;
|
|
509
|
+
constructor(config) {
|
|
510
|
+
this.after = config.after;
|
|
511
|
+
this.cursorColumn = config.cursorColumn || "id";
|
|
512
|
+
}
|
|
513
|
+
transformQuery(args) {
|
|
514
|
+
if (this.after === undefined)
|
|
515
|
+
return args.node;
|
|
516
|
+
const { node } = args;
|
|
517
|
+
if (node.kind === "SelectQueryNode") {}
|
|
518
|
+
return node;
|
|
519
|
+
}
|
|
520
|
+
async transformResult(args) {
|
|
521
|
+
return args.result;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/db/kysely/plugins/deadlock-handler.ts
|
|
526
|
+
class DeadlockRetryPlugin {
|
|
527
|
+
maxRetries;
|
|
528
|
+
initialDelay;
|
|
529
|
+
constructor(config) {
|
|
530
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
531
|
+
this.initialDelay = config.initialDelay ?? 50;
|
|
532
|
+
}
|
|
533
|
+
transformQuery(args) {
|
|
534
|
+
return args.node;
|
|
535
|
+
}
|
|
536
|
+
async transformResult(args) {
|
|
537
|
+
return args.result;
|
|
538
|
+
}
|
|
539
|
+
async retry(fn) {
|
|
540
|
+
let lastError;
|
|
541
|
+
for (let attempt = 0;attempt <= this.maxRetries; attempt++) {
|
|
542
|
+
try {
|
|
543
|
+
return await fn();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
lastError = error;
|
|
546
|
+
const isLockError = error?.message?.includes("SQLITE_BUSY") || error?.message?.includes("database is locked") || error?.code === "SQLITE_BUSY";
|
|
547
|
+
if (!isLockError || attempt >= this.maxRetries) {
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
const delay = this.initialDelay * 2 ** attempt;
|
|
551
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
throw lastError;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/db/kysely/plugins/draft-swapper.ts
|
|
559
|
+
class DraftSwapperPlugin {
|
|
560
|
+
draftMode;
|
|
561
|
+
tables;
|
|
562
|
+
constructor(config) {
|
|
563
|
+
this.draftMode = config.draftMode ?? false;
|
|
564
|
+
const allSlugs = [
|
|
565
|
+
...config.collections?.map((c) => c.slug) || [],
|
|
566
|
+
...config.globals?.map((g) => g.slug) || []
|
|
567
|
+
];
|
|
568
|
+
this.tables = new Set(allSlugs.map((s) => toSnakeCase(s)));
|
|
569
|
+
}
|
|
570
|
+
transformQuery(args) {
|
|
571
|
+
if (!this.draftMode)
|
|
572
|
+
return args.node;
|
|
573
|
+
const { node } = args;
|
|
574
|
+
if (node.kind === "SelectQueryNode") {
|
|
575
|
+
if (node.from && node.from.froms) {
|
|
576
|
+
let shouldSwap = false;
|
|
577
|
+
let originalTable = "";
|
|
578
|
+
const newFroms = node.from.froms.map((f) => {
|
|
579
|
+
if (f.kind === "TableNode" && f.table && f.table.kind === "IdentifierNode") {
|
|
580
|
+
const tableName = f.table.name;
|
|
581
|
+
if (this.tables?.has(tableName)) {
|
|
582
|
+
shouldSwap = true;
|
|
583
|
+
originalTable = tableName;
|
|
584
|
+
return {
|
|
585
|
+
...f,
|
|
586
|
+
table: {
|
|
587
|
+
...f.table,
|
|
588
|
+
name: "_doc_versions"
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return f;
|
|
594
|
+
});
|
|
595
|
+
if (shouldSwap) {
|
|
596
|
+
node.from.froms = newFroms;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return node;
|
|
601
|
+
}
|
|
602
|
+
async transformResult(args) {
|
|
603
|
+
const { result } = args;
|
|
604
|
+
if (!this.draftMode || result.rows.length === 0)
|
|
605
|
+
return result;
|
|
606
|
+
if (result.rows[0] && "data" in result.rows[0] && "collection" in result.rows[0]) {
|
|
607
|
+
const mappedRows = result.rows.map((row) => {
|
|
608
|
+
try {
|
|
609
|
+
const parsedData = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
|
|
610
|
+
return {
|
|
611
|
+
...parsedData,
|
|
612
|
+
_version_id: row.id,
|
|
613
|
+
_version_status: row.status,
|
|
614
|
+
_version_createdAt: row.createdAt
|
|
615
|
+
};
|
|
616
|
+
} catch {
|
|
617
|
+
return row;
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
return { ...result, rows: mappedRows };
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
var init_draft_swapper = () => {};
|
|
626
|
+
|
|
627
|
+
// src/db/kysely/plugins/field-masking.ts
|
|
628
|
+
class FieldMaskingPlugin {
|
|
629
|
+
maskedFields = new Map;
|
|
630
|
+
disabled;
|
|
631
|
+
constructor(config) {
|
|
632
|
+
this.disabled = config.disabled ?? false;
|
|
633
|
+
this.parseConfig(config);
|
|
634
|
+
}
|
|
635
|
+
parseConfig(config) {
|
|
636
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
637
|
+
for (const res of allResources) {
|
|
638
|
+
if (res.slug.startsWith("_"))
|
|
639
|
+
continue;
|
|
640
|
+
const tableName = toSnakeCase(res.slug);
|
|
641
|
+
const fields = new Set;
|
|
642
|
+
const findMaskedFields = (fieldList) => {
|
|
643
|
+
for (const f of fieldList) {
|
|
644
|
+
if (f.name && (f.admin?.hidden === true || f.type === "password")) {
|
|
645
|
+
fields.add(toSnakeCase(f.name));
|
|
646
|
+
}
|
|
647
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
648
|
+
findMaskedFields(f.fields);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
findMaskedFields(res.fields);
|
|
653
|
+
if (fields.size > 0) {
|
|
654
|
+
this.maskedFields.set(tableName, fields);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
transformQuery(args) {
|
|
659
|
+
return args.node;
|
|
660
|
+
}
|
|
661
|
+
async transformResult(args) {
|
|
662
|
+
const { result } = args;
|
|
663
|
+
if (this.disabled)
|
|
664
|
+
return result;
|
|
665
|
+
return {
|
|
666
|
+
...result,
|
|
667
|
+
rows: result.rows.map((row) => this.maskRow(row))
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
maskRow(row) {
|
|
671
|
+
if (!row)
|
|
672
|
+
return row;
|
|
673
|
+
const newRow = { ...row };
|
|
674
|
+
for (const [_, fields] of this.maskedFields.entries()) {
|
|
675
|
+
for (const field of fields) {
|
|
676
|
+
if (field in newRow) {
|
|
677
|
+
if (field.includes("password") || field.includes("hash")) {
|
|
678
|
+
newRow[field] = "********";
|
|
679
|
+
} else {
|
|
680
|
+
delete newRow[field];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return newRow;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
var init_field_masking = () => {};
|
|
689
|
+
|
|
690
|
+
// src/db/kysely/plugins/fts-normalizer.ts
|
|
691
|
+
class FtsNormalizerPlugin {
|
|
692
|
+
dialect;
|
|
693
|
+
constructor(config) {
|
|
694
|
+
this.dialect = config.dialect;
|
|
695
|
+
}
|
|
696
|
+
transformQuery(args) {
|
|
697
|
+
const { node } = args;
|
|
698
|
+
return node;
|
|
699
|
+
}
|
|
700
|
+
async transformResult(args) {
|
|
701
|
+
return args.result;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/db/kysely/plugins/i18n-fallback.ts
|
|
706
|
+
class AutoTranslationFallbackPlugin {
|
|
707
|
+
localizedFields = new Map;
|
|
708
|
+
config;
|
|
709
|
+
constructor(config) {
|
|
710
|
+
this.config = config;
|
|
711
|
+
this.parseConfig(config);
|
|
712
|
+
}
|
|
713
|
+
parseConfig(config) {
|
|
714
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
715
|
+
for (const res of allResources) {
|
|
716
|
+
const tableName = toSnakeCase(res.slug);
|
|
717
|
+
const fields = new Set;
|
|
718
|
+
const findLocalizedFields = (fieldList) => {
|
|
719
|
+
for (const f of fieldList) {
|
|
720
|
+
if (f.name && f.localized) {
|
|
721
|
+
fields.add(toSnakeCase(f.name));
|
|
722
|
+
}
|
|
723
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
724
|
+
findLocalizedFields(f.fields);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
findLocalizedFields(res.fields);
|
|
729
|
+
if (fields.size > 0) {
|
|
730
|
+
this.localizedFields.set(tableName, fields);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
transformQuery(args) {
|
|
735
|
+
return args.node;
|
|
736
|
+
}
|
|
737
|
+
async transformResult(args) {
|
|
738
|
+
const { result } = args;
|
|
739
|
+
if (this.config.defaultLocale === this.config.currentLocale)
|
|
740
|
+
return result;
|
|
741
|
+
return {
|
|
742
|
+
...result,
|
|
743
|
+
rows: result.rows.map((row) => this.fallbackRow(row))
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
fallbackRow(row) {
|
|
747
|
+
if (!row)
|
|
748
|
+
return row;
|
|
749
|
+
const newRow = { ...row };
|
|
750
|
+
const currentLocSuffix = `_${this.config.currentLocale}`;
|
|
751
|
+
const defaultLocSuffix = `_${this.config.defaultLocale}`;
|
|
752
|
+
for (const [key, value] of Object.entries(newRow)) {
|
|
753
|
+
if (key.endsWith(currentLocSuffix)) {
|
|
754
|
+
const baseName = key.slice(0, -currentLocSuffix.length);
|
|
755
|
+
const defaultValueKey = `${baseName}${defaultLocSuffix}`;
|
|
756
|
+
if ((value === null || value === "" || value === undefined) && newRow[defaultValueKey] !== undefined) {
|
|
757
|
+
newRow[key] = newRow[defaultValueKey];
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return newRow;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
var init_i18n_fallback = () => {};
|
|
765
|
+
|
|
766
|
+
// src/db/kysely/plugins/id-generation.ts
|
|
767
|
+
class IdGenerationPlugin {
|
|
768
|
+
generateId;
|
|
769
|
+
idColumn;
|
|
770
|
+
constructor(config = {}) {
|
|
771
|
+
this.generateId = config.generateId || (() => crypto.randomUUID());
|
|
772
|
+
this.idColumn = config.idColumn || "id";
|
|
773
|
+
}
|
|
774
|
+
transformQuery(args) {
|
|
775
|
+
const { node } = args;
|
|
776
|
+
if (node.kind === "InsertQueryNode") {}
|
|
777
|
+
return node;
|
|
778
|
+
}
|
|
779
|
+
async transformResult(args) {
|
|
780
|
+
return args.result;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/db/kysely/plugins/json-flattener.ts
|
|
785
|
+
class JsonFlattenerPlugin {
|
|
786
|
+
jsonFields = new Map;
|
|
787
|
+
constructor(config) {
|
|
788
|
+
this.parseConfig(config);
|
|
789
|
+
}
|
|
790
|
+
parseConfig(config) {
|
|
791
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
792
|
+
for (const res of allResources) {
|
|
793
|
+
const tableName = toSnakeCase(res.slug);
|
|
794
|
+
const fields = new Set;
|
|
795
|
+
const findJsonFields = (fieldList) => {
|
|
796
|
+
for (const f of fieldList) {
|
|
797
|
+
if (f.name && (["richtext", "json", "file", "blocks"].includes(f.type) || f.localized)) {
|
|
798
|
+
fields.add(toSnakeCase(f.name));
|
|
799
|
+
}
|
|
800
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
801
|
+
findJsonFields(f.fields);
|
|
802
|
+
}
|
|
803
|
+
if ("blocks" in f && Array.isArray(f.blocks)) {
|
|
804
|
+
for (const b of f.blocks) {
|
|
805
|
+
if (b.fields)
|
|
806
|
+
findJsonFields(b.fields);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if ("tabs" in f && Array.isArray(f.tabs)) {
|
|
810
|
+
for (const t of f.tabs) {
|
|
811
|
+
if (t.fields)
|
|
812
|
+
findJsonFields(t.fields);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
findJsonFields(res.fields);
|
|
818
|
+
if (fields.size > 0) {
|
|
819
|
+
this.jsonFields.set(tableName, fields);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
transformQuery(args) {
|
|
824
|
+
const { node } = args;
|
|
825
|
+
if (node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode") {}
|
|
826
|
+
return node;
|
|
827
|
+
}
|
|
828
|
+
async transformResult(args) {
|
|
829
|
+
const { result } = args;
|
|
830
|
+
return {
|
|
831
|
+
...result,
|
|
832
|
+
rows: result.rows.map((row) => this.mapRow(row))
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
mapRow(row) {
|
|
836
|
+
if (!row)
|
|
837
|
+
return row;
|
|
838
|
+
const newRow = { ...row };
|
|
839
|
+
const isSystemTable = Object.keys(newRow).some((k) => k.startsWith("_"));
|
|
840
|
+
if (isSystemTable)
|
|
841
|
+
return newRow;
|
|
842
|
+
for (const [key, value] of Object.entries(newRow)) {
|
|
843
|
+
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
|
|
844
|
+
try {
|
|
845
|
+
newRow[key] = JSON.parse(value);
|
|
846
|
+
} catch (_e) {}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return newRow;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
var init_json_flattener = () => {};
|
|
853
|
+
|
|
854
|
+
// src/db/kysely/plugins/relationship-preloading.ts
|
|
855
|
+
class RelationshipPreloadingPlugin {
|
|
856
|
+
maxDepth;
|
|
857
|
+
constructor(config = {}) {
|
|
858
|
+
this.maxDepth = config.maxDepth ?? 2;
|
|
859
|
+
}
|
|
860
|
+
transformQuery(args) {
|
|
861
|
+
return args.node;
|
|
862
|
+
}
|
|
863
|
+
async transformResult(args) {
|
|
864
|
+
const { result } = args;
|
|
865
|
+
if (result.rows.length === 0)
|
|
866
|
+
return result;
|
|
867
|
+
return result;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/db/kysely/plugins/slug-generation.ts
|
|
872
|
+
class SlugGenerationPlugin {
|
|
873
|
+
sourceColumn;
|
|
874
|
+
slugColumn;
|
|
875
|
+
constructor(config = {}) {
|
|
876
|
+
this.sourceColumn = config.sourceColumn || "title";
|
|
877
|
+
this.slugColumn = config.slugColumn || "slug";
|
|
878
|
+
}
|
|
879
|
+
transformQuery(args) {
|
|
880
|
+
const { node } = args;
|
|
881
|
+
if (node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode") {}
|
|
882
|
+
return node;
|
|
883
|
+
}
|
|
884
|
+
async transformResult(args) {
|
|
885
|
+
return args.result;
|
|
886
|
+
}
|
|
887
|
+
slugify(text) {
|
|
888
|
+
return text.toString().toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/db/kysely/plugins/soft-delete.ts
|
|
893
|
+
class SoftDeletePlugin {
|
|
894
|
+
deletedAtColumn;
|
|
895
|
+
tables;
|
|
896
|
+
constructor(config = {}) {
|
|
897
|
+
this.deletedAtColumn = config.deletedAtColumn || "deleted_at";
|
|
898
|
+
this.tables = config.tables ? new Set(config.tables) : null;
|
|
899
|
+
}
|
|
900
|
+
transformQuery(args) {
|
|
901
|
+
const { node } = args;
|
|
902
|
+
if (node.kind === "DeleteQueryNode") {} else if (node.kind === "SelectQueryNode") {}
|
|
903
|
+
return node;
|
|
904
|
+
}
|
|
905
|
+
async transformResult(args) {
|
|
906
|
+
return args.result;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/db/kysely/plugins/tree-resolver.ts
|
|
911
|
+
class TreeResolverPlugin {
|
|
912
|
+
parentColumn;
|
|
913
|
+
childrenProperty;
|
|
914
|
+
constructor(config) {
|
|
915
|
+
this.parentColumn = config.parentColumn || "parent_id";
|
|
916
|
+
this.childrenProperty = config.childrenProperty || "children";
|
|
917
|
+
}
|
|
918
|
+
transformQuery(args) {
|
|
919
|
+
return args.node;
|
|
920
|
+
}
|
|
921
|
+
async transformResult(args) {
|
|
922
|
+
const { result } = args;
|
|
923
|
+
if (result.rows.length === 0)
|
|
924
|
+
return result;
|
|
925
|
+
return {
|
|
926
|
+
...result,
|
|
927
|
+
rows: this.buildTree(result.rows)
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
buildTree(rows) {
|
|
931
|
+
const map = new Map;
|
|
932
|
+
const roots = [];
|
|
933
|
+
for (const row of rows) {
|
|
934
|
+
map.set(row.id, { ...row, [this.childrenProperty]: [] });
|
|
935
|
+
}
|
|
936
|
+
for (const row of rows) {
|
|
937
|
+
const parentId = row[this.parentColumn];
|
|
938
|
+
const item = map.get(row.id);
|
|
939
|
+
if (parentId && map.has(parentId)) {
|
|
940
|
+
map.get(parentId)[this.childrenProperty].push(item);
|
|
941
|
+
} else {
|
|
942
|
+
roots.push(item);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return roots;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/db/kysely/plugins/virtual-field-resolver.ts
|
|
950
|
+
class VirtualFieldResolverPlugin {
|
|
951
|
+
resolvers = new Map;
|
|
952
|
+
config;
|
|
953
|
+
constructor(config) {
|
|
954
|
+
this.config = config;
|
|
955
|
+
this.parseConfig(config);
|
|
956
|
+
}
|
|
957
|
+
parseConfig(config) {
|
|
958
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
959
|
+
for (const res of allResources) {
|
|
960
|
+
const tableName = toSnakeCase(res.slug);
|
|
961
|
+
const resResolvers = new Map;
|
|
962
|
+
const findResolvers = (fieldList) => {
|
|
963
|
+
for (const f of fieldList) {
|
|
964
|
+
if (f.name && f.type === "virtual" && typeof f.resolve === "function") {
|
|
965
|
+
resResolvers.set(f.name, f.resolve);
|
|
966
|
+
} else if (f.name && f.resolve && typeof f.resolve === "function") {
|
|
967
|
+
resResolvers.set(f.name, f.resolve);
|
|
968
|
+
}
|
|
969
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
970
|
+
findResolvers(f.fields);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
findResolvers(res.fields);
|
|
975
|
+
if (resResolvers.size > 0) {
|
|
976
|
+
this.resolvers.set(tableName, resResolvers);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
transformQuery(args) {
|
|
981
|
+
return args.node;
|
|
982
|
+
}
|
|
983
|
+
async transformResult(args) {
|
|
984
|
+
const { result } = args;
|
|
985
|
+
if (result.rows.length === 0)
|
|
986
|
+
return result;
|
|
987
|
+
const rows = await Promise.all(result.rows.map(async (row) => {
|
|
988
|
+
const newRow = { ...row };
|
|
989
|
+
for (const [tableName, tableResolvers] of this.resolvers.entries()) {
|
|
990
|
+
for (const [fieldName, resolveFn] of tableResolvers.entries()) {
|
|
991
|
+
try {
|
|
992
|
+
newRow[fieldName] = await resolveFn({
|
|
993
|
+
data: row,
|
|
994
|
+
req: this.config.req,
|
|
995
|
+
user: this.config.user,
|
|
996
|
+
session: this.config.session,
|
|
997
|
+
apiKey: this.config.apiKey
|
|
998
|
+
});
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
console.error(`Error resolving virtual field ${fieldName} for table ${tableName}:`, e);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return newRow;
|
|
1005
|
+
}));
|
|
1006
|
+
return {
|
|
1007
|
+
...result,
|
|
1008
|
+
rows
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
var init_virtual_field_resolver = () => {};
|
|
1013
|
+
|
|
1014
|
+
// src/db/kysely/plugins/zod-coercion.ts
|
|
1015
|
+
class ZodCoercionPlugin {
|
|
1016
|
+
schemas;
|
|
1017
|
+
constructor(config) {
|
|
1018
|
+
this.schemas = config.schemas;
|
|
1019
|
+
}
|
|
1020
|
+
transformQuery(args) {
|
|
1021
|
+
return args.node;
|
|
1022
|
+
}
|
|
1023
|
+
async transformResult(args) {
|
|
1024
|
+
const { result } = args;
|
|
1025
|
+
if (result.rows.length === 0)
|
|
1026
|
+
return result;
|
|
1027
|
+
const coercedRows = result.rows.map((row) => {
|
|
1028
|
+
return row;
|
|
1029
|
+
});
|
|
1030
|
+
return {
|
|
1031
|
+
...result,
|
|
1032
|
+
rows: coercedRows
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/db/kysely/plugins/index.ts
|
|
1038
|
+
var init_plugins = __esm(() => {
|
|
1039
|
+
init_audit_logging();
|
|
1040
|
+
init_draft_swapper();
|
|
1041
|
+
init_field_masking();
|
|
1042
|
+
init_i18n_fallback();
|
|
1043
|
+
init_json_flattener();
|
|
1044
|
+
init_virtual_field_resolver();
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// src/db/kysely/factory.ts
|
|
1048
|
+
var exports_factory = {};
|
|
1049
|
+
__export(exports_factory, {
|
|
1050
|
+
createOpacaKysely: () => createOpacaKysely
|
|
1051
|
+
});
|
|
1052
|
+
import { CamelCasePlugin, Kysely } from "kysely";
|
|
1053
|
+
function createOpacaKysely(options) {
|
|
1054
|
+
const { dialect, config, isAdmin = false } = options;
|
|
1055
|
+
const plugins = [
|
|
1056
|
+
new JsonFlattenerPlugin({
|
|
1057
|
+
collections: config.collections,
|
|
1058
|
+
globals: config.globals
|
|
1059
|
+
}),
|
|
1060
|
+
new ZodCoercionPlugin({
|
|
1061
|
+
schemas: {}
|
|
1062
|
+
}),
|
|
1063
|
+
new AutoTimestampsPlugin,
|
|
1064
|
+
new IdGenerationPlugin,
|
|
1065
|
+
new SlugGenerationPlugin,
|
|
1066
|
+
new AuditLoggingPlugin({
|
|
1067
|
+
auditTable: "_audit_logs",
|
|
1068
|
+
getUserId: options.getUserId
|
|
1069
|
+
}),
|
|
1070
|
+
new SoftDeletePlugin,
|
|
1071
|
+
new DraftSwapperPlugin({
|
|
1072
|
+
draftMode: false,
|
|
1073
|
+
collections: config.collections,
|
|
1074
|
+
globals: config.globals
|
|
1075
|
+
}),
|
|
1076
|
+
new AutoTranslationFallbackPlugin({
|
|
1077
|
+
collections: config.collections,
|
|
1078
|
+
globals: config.globals,
|
|
1079
|
+
defaultLocale: config.i18n?.defaultLocale || "en",
|
|
1080
|
+
currentLocale: config.i18n?.defaultLocale || "en"
|
|
1081
|
+
}),
|
|
1082
|
+
new CursorPaginationPlugin({}),
|
|
1083
|
+
new FtsNormalizerPlugin({
|
|
1084
|
+
dialect: config.db.name
|
|
1085
|
+
}),
|
|
1086
|
+
new TreeResolverPlugin({}),
|
|
1087
|
+
new VirtualFieldResolverPlugin({
|
|
1088
|
+
collections: config.collections,
|
|
1089
|
+
globals: config.globals
|
|
1090
|
+
}),
|
|
1091
|
+
new RelationshipPreloadingPlugin({}),
|
|
1092
|
+
new DeadlockRetryPlugin({ maxRetries: 5 })
|
|
1093
|
+
];
|
|
1094
|
+
const db = new Kysely({
|
|
1095
|
+
dialect,
|
|
1096
|
+
plugins: [new CamelCasePlugin, ...plugins]
|
|
1097
|
+
});
|
|
1098
|
+
for (const plugin of plugins) {
|
|
1099
|
+
if (plugin && typeof plugin.setDb === "function") {
|
|
1100
|
+
plugin.setDb(db);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return db;
|
|
1104
|
+
}
|
|
1105
|
+
var init_factory = __esm(() => {
|
|
1106
|
+
init_plugins();
|
|
1107
|
+
});
|
|
25
1108
|
|
|
26
1109
|
// src/db/sqlite.ts
|
|
27
|
-
import fs from "node:fs/promises";
|
|
28
|
-
import path from "node:path";
|
|
29
|
-
import { pathToFileURL } from "node:url";
|
|
30
1110
|
import {
|
|
31
1111
|
CompiledQuery,
|
|
32
1112
|
FileMigrationProvider,
|
|
33
1113
|
Migrator,
|
|
34
1114
|
SqliteDialect
|
|
35
1115
|
} from "kysely";
|
|
1116
|
+
|
|
1117
|
+
// src/db/adapter.ts
|
|
1118
|
+
class BaseDatabaseAdapter {
|
|
1119
|
+
get raw() {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
get db() {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
push;
|
|
1126
|
+
pushDestructive;
|
|
1127
|
+
migrationDir;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/db/kysely/data-mapper.ts
|
|
1131
|
+
function toCamelCase(str) {
|
|
1132
|
+
if (str.startsWith("_"))
|
|
1133
|
+
return str;
|
|
1134
|
+
return str.replace(/_+([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1135
|
+
}
|
|
1136
|
+
function flattenPayload(payload, prefix = "", excludeKeys = []) {
|
|
1137
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload))
|
|
1138
|
+
return payload;
|
|
1139
|
+
const result = {};
|
|
1140
|
+
for (const key in payload) {
|
|
1141
|
+
if (Object.hasOwn(payload, key)) {
|
|
1142
|
+
const value = payload[key];
|
|
1143
|
+
if (value === undefined)
|
|
1144
|
+
continue;
|
|
1145
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !excludeKeys.includes(key)) {
|
|
1146
|
+
if (key.startsWith("_") && prefix === "") {
|
|
1147
|
+
result[key] = value;
|
|
1148
|
+
} else {
|
|
1149
|
+
const flatNested = flattenPayload(value, `${prefix}${key}__`);
|
|
1150
|
+
Object.assign(result, flatNested);
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
if (value === undefined)
|
|
1154
|
+
continue;
|
|
1155
|
+
const colName = `${prefix}${toSnakeCase(key)}`;
|
|
1156
|
+
if (typeof value === "object" && value !== null && !(value instanceof Date)) {
|
|
1157
|
+
result[colName] = JSON.stringify(value);
|
|
1158
|
+
} else {
|
|
1159
|
+
result[colName] = value;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
function unflattenRow(row) {
|
|
1167
|
+
if (typeof row !== "object" || row === null || Array.isArray(row))
|
|
1168
|
+
return row;
|
|
1169
|
+
const result = {};
|
|
1170
|
+
for (const key in row) {
|
|
1171
|
+
if (Object.hasOwn(row, key)) {
|
|
1172
|
+
let value = row[key];
|
|
1173
|
+
if (typeof value === "string") {
|
|
1174
|
+
const trimmed = value.trim();
|
|
1175
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]") || trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1176
|
+
try {
|
|
1177
|
+
value = JSON.parse(value);
|
|
1178
|
+
} catch (e) {}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (key === "created_at" || key === "updated_at" || key === "createdAt" || key === "updatedAt") {
|
|
1182
|
+
if (typeof value === "string" && !value.includes("T") && value.includes(" ")) {
|
|
1183
|
+
value = `${value.replace(" ", "T")}Z`;
|
|
1184
|
+
}
|
|
1185
|
+
result[toCamelCase(key)] = value;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
if (key.startsWith("_")) {
|
|
1189
|
+
result[key] = value;
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const parts = key.split("__");
|
|
1193
|
+
if (parts.length === 1) {
|
|
1194
|
+
result[toCamelCase(key)] = value;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
let current = result;
|
|
1198
|
+
let collision = false;
|
|
1199
|
+
const path = [];
|
|
1200
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
1201
|
+
const part = toCamelCase(parts[i]);
|
|
1202
|
+
if (current[part] !== undefined && (typeof current[part] !== "object" || current[part] === null)) {
|
|
1203
|
+
collision = true;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
path.push({ obj: current, key: part });
|
|
1207
|
+
if (!current[part]) {
|
|
1208
|
+
current[part] = {};
|
|
1209
|
+
}
|
|
1210
|
+
current = current[part];
|
|
1211
|
+
}
|
|
1212
|
+
if (!collision) {
|
|
1213
|
+
const lastPart = toCamelCase(parts[parts.length - 1]);
|
|
1214
|
+
if (current[lastPart] !== undefined && typeof current[lastPart] === "object") {
|
|
1215
|
+
collision = true;
|
|
1216
|
+
} else {
|
|
1217
|
+
current[lastPart] = value;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (collision) {
|
|
1221
|
+
result[toCamelCase(key)] = value;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return result;
|
|
1226
|
+
}
|
|
1227
|
+
// src/db/kysely/query-builder.ts
|
|
1228
|
+
function buildKyselyWhere(eb, query) {
|
|
1229
|
+
if (!query || Object.keys(query).length === 0)
|
|
1230
|
+
return;
|
|
1231
|
+
const conditions = [];
|
|
1232
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1233
|
+
if (value === undefined)
|
|
1234
|
+
continue;
|
|
1235
|
+
if (key === "or" && Array.isArray(value)) {
|
|
1236
|
+
const orConditions = value.map((v) => buildKyselyWhere(eb, v)).filter((c) => c !== undefined);
|
|
1237
|
+
if (orConditions.length > 0) {
|
|
1238
|
+
conditions.push(eb.or(orConditions));
|
|
1239
|
+
}
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
if (key === "and" && Array.isArray(value)) {
|
|
1243
|
+
const andConditions = value.map((v) => buildKyselyWhere(eb, v)).filter((c) => c !== undefined);
|
|
1244
|
+
if (andConditions.length > 0) {
|
|
1245
|
+
conditions.push(eb.and(andConditions));
|
|
1246
|
+
}
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
|
|
1250
|
+
for (const [op, val] of Object.entries(value)) {
|
|
1251
|
+
if (val === undefined)
|
|
1252
|
+
continue;
|
|
1253
|
+
switch (op) {
|
|
1254
|
+
case "gt":
|
|
1255
|
+
conditions.push(eb(key, ">", val));
|
|
1256
|
+
break;
|
|
1257
|
+
case "gte":
|
|
1258
|
+
conditions.push(eb(key, ">=", val));
|
|
1259
|
+
break;
|
|
1260
|
+
case "lt":
|
|
1261
|
+
conditions.push(eb(key, "<", val));
|
|
1262
|
+
break;
|
|
1263
|
+
case "lte":
|
|
1264
|
+
conditions.push(eb(key, "<=", val));
|
|
1265
|
+
break;
|
|
1266
|
+
case "like":
|
|
1267
|
+
conditions.push(eb(key, "like", String(val)));
|
|
1268
|
+
break;
|
|
1269
|
+
case "ne":
|
|
1270
|
+
if (val === null)
|
|
1271
|
+
conditions.push(eb(key, "is not", null));
|
|
1272
|
+
else
|
|
1273
|
+
conditions.push(eb(key, "!=", val));
|
|
1274
|
+
break;
|
|
1275
|
+
case "in":
|
|
1276
|
+
if (Array.isArray(val))
|
|
1277
|
+
conditions.push(eb(key, "in", val));
|
|
1278
|
+
break;
|
|
1279
|
+
case "is":
|
|
1280
|
+
if (val === null)
|
|
1281
|
+
conditions.push(eb(key, "is", null));
|
|
1282
|
+
break;
|
|
1283
|
+
default:
|
|
1284
|
+
if (val === null)
|
|
1285
|
+
conditions.push(eb(key, "is", null));
|
|
1286
|
+
else
|
|
1287
|
+
conditions.push(eb(key, "=", val));
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
if (value === null) {
|
|
1293
|
+
conditions.push(eb(key, "is", null));
|
|
1294
|
+
} else {
|
|
1295
|
+
conditions.push(eb(key, "=", value));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (conditions.length === 0)
|
|
1300
|
+
return;
|
|
1301
|
+
if (conditions.length === 1)
|
|
1302
|
+
return conditions[0];
|
|
1303
|
+
return eb.and(conditions);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/db/kysely/schema-builder.ts
|
|
1307
|
+
import { sql } from "kysely";
|
|
1308
|
+
|
|
1309
|
+
// src/db/kysely/sql-utils.ts
|
|
1310
|
+
function assertSafeIdentifier(identifier) {
|
|
1311
|
+
if (typeof identifier !== "string" || !identifier) {
|
|
1312
|
+
throw new Error(`Invalid identifier: must be a non-empty string. Got: ${identifier}`);
|
|
1313
|
+
}
|
|
1314
|
+
const isValid = /^[a-zA-Z0-9_-]+$/.test(identifier);
|
|
1315
|
+
if (!isValid) {
|
|
1316
|
+
throw new Error(`API Error: Unsafe SQL identifier detected: "${identifier}". Identifiers can only contain alphanumeric characters, underscores, and hyphens.`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
// src/utils/logger.ts
|
|
1320
|
+
var RESET = "\x1B[0m";
|
|
1321
|
+
var BOLD = "\x1B[1m";
|
|
1322
|
+
var BLUE = "\x1B[34m";
|
|
1323
|
+
var GREEN = "\x1B[32m";
|
|
1324
|
+
var YELLOW = "\x1B[33m";
|
|
1325
|
+
var RED = "\x1B[31m";
|
|
1326
|
+
var GRAY = "\x1B[90m";
|
|
1327
|
+
var PREFIX = `${BLUE}${BOLD}[OpacaCMS]${RESET}`;
|
|
1328
|
+
var LOG_LEVELS = {
|
|
1329
|
+
debug: 0,
|
|
1330
|
+
info: 1,
|
|
1331
|
+
warn: 2,
|
|
1332
|
+
error: 3
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
class OpacaLogger {
|
|
1336
|
+
config;
|
|
1337
|
+
constructor(config = {}) {
|
|
1338
|
+
this.config = config;
|
|
1339
|
+
}
|
|
1340
|
+
shouldLog(level) {
|
|
1341
|
+
if (this.config.disabled)
|
|
1342
|
+
return false;
|
|
1343
|
+
const configLevel = this.config.level || "info";
|
|
1344
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[configLevel];
|
|
1345
|
+
}
|
|
1346
|
+
info(message, ...args) {
|
|
1347
|
+
if (!this.shouldLog("info"))
|
|
1348
|
+
return;
|
|
1349
|
+
console.log(`${PREFIX} ${message}`, ...args);
|
|
1350
|
+
}
|
|
1351
|
+
success(message, ...args) {
|
|
1352
|
+
if (!this.shouldLog("info"))
|
|
1353
|
+
return;
|
|
1354
|
+
console.log(`${PREFIX} ${GREEN}${message}${RESET}`, ...args);
|
|
1355
|
+
}
|
|
1356
|
+
debug(message, ...args) {
|
|
1357
|
+
if (!this.shouldLog("debug"))
|
|
1358
|
+
return;
|
|
1359
|
+
console.log(`${PREFIX} ${GRAY}${message}${RESET}`, ...args);
|
|
1360
|
+
}
|
|
1361
|
+
warn(message, ...args) {
|
|
1362
|
+
if (!this.shouldLog("warn"))
|
|
1363
|
+
return;
|
|
1364
|
+
console.warn(`${PREFIX} ${YELLOW}Warning: ${message}${RESET}`, ...args);
|
|
1365
|
+
}
|
|
1366
|
+
error(message, ...args) {
|
|
1367
|
+
if (!this.shouldLog("error"))
|
|
1368
|
+
return;
|
|
1369
|
+
console.error(`${PREFIX} ${RED}Error: ${message}${RESET}`, ...args);
|
|
1370
|
+
}
|
|
1371
|
+
log(message, ...args) {
|
|
1372
|
+
if (this.config.disabled)
|
|
1373
|
+
return;
|
|
1374
|
+
console.log(message, ...args);
|
|
1375
|
+
}
|
|
1376
|
+
bold(msg) {
|
|
1377
|
+
if (this.config.disableColors)
|
|
1378
|
+
return msg;
|
|
1379
|
+
return `${BOLD}${msg}${RESET}`;
|
|
1380
|
+
}
|
|
1381
|
+
format(color, msg) {
|
|
1382
|
+
if (this.config.disableColors)
|
|
1383
|
+
return msg;
|
|
1384
|
+
switch (color) {
|
|
1385
|
+
case "green":
|
|
1386
|
+
return `${GREEN}${msg}${RESET}`;
|
|
1387
|
+
case "red":
|
|
1388
|
+
return `${RED}${msg}${RESET}`;
|
|
1389
|
+
case "yellow":
|
|
1390
|
+
return `${YELLOW}${msg}${RESET}`;
|
|
1391
|
+
case "gray":
|
|
1392
|
+
return `${GRAY}${msg}${RESET}`;
|
|
1393
|
+
default:
|
|
1394
|
+
return msg;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
var logger = new OpacaLogger;
|
|
1399
|
+
|
|
1400
|
+
// src/db/kysely/schema-builder.ts
|
|
1401
|
+
async function pushSchema(db, dialect, collections, globals = [], options = {}) {
|
|
1402
|
+
const rawSchemas = [...getSystemCollections(), ...collections, ...globals];
|
|
1403
|
+
const allSchemas = [];
|
|
1404
|
+
const seenSlugs = new Set;
|
|
1405
|
+
for (const schema of rawSchemas) {
|
|
1406
|
+
if (!seenSlugs.has(schema.slug)) {
|
|
1407
|
+
seenSlugs.add(schema.slug);
|
|
1408
|
+
allSchemas.push(schema);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const { pushDestructive } = options;
|
|
1412
|
+
logger.debug(`Starting schema push via Kysely (${dialect})...`);
|
|
1413
|
+
const mapType = dialect === "postgres" ? mapFieldToPostgresType : mapFieldToSQLiteType;
|
|
1414
|
+
let tablesMetadata = [];
|
|
1415
|
+
try {
|
|
1416
|
+
tablesMetadata = await db.introspection.getTables();
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
logger.warn("Failed to fetch database introspection. Falling back to empty state.", e);
|
|
1419
|
+
}
|
|
1420
|
+
const existingTableMap = new Map(tablesMetadata.map((t) => [t.name, t]));
|
|
1421
|
+
const existingTables = Array.from(existingTableMap.keys());
|
|
1422
|
+
logger.debug(`Existing tables found: ${existingTables.join(", ")}`);
|
|
1423
|
+
const expectedTableNames = new Set;
|
|
1424
|
+
for (const collection of allSchemas) {
|
|
1425
|
+
const slug = collection.slug;
|
|
1426
|
+
const tableName = toSnakeCase(slug);
|
|
1427
|
+
assertSafeIdentifier(tableName);
|
|
1428
|
+
expectedTableNames.add(tableName);
|
|
1429
|
+
const flattenedFields = flattenFields(collection.fields);
|
|
1430
|
+
const hasTimestamps = true;
|
|
1431
|
+
const versions = collection.versions;
|
|
1432
|
+
const existingTable = existingTableMap.get(tableName);
|
|
1433
|
+
if (!existingTable) {
|
|
1434
|
+
logger.info(` ${logger.format("green", "✔")} Creating table: ${logger.bold(`"${tableName}"`)}`);
|
|
1435
|
+
let builder = db.schema.createTable(tableName).ifNotExists();
|
|
1436
|
+
builder = builder.addColumn("id", "text", (col) => col.primaryKey());
|
|
1437
|
+
for (const field of flattenedFields) {
|
|
1438
|
+
if (field.type === "relationship" && "hasMany" in field && field.hasMany)
|
|
1439
|
+
continue;
|
|
1440
|
+
const colName = toSnakeCase(field.name);
|
|
1441
|
+
if (colName === "id")
|
|
1442
|
+
continue;
|
|
1443
|
+
assertSafeIdentifier(colName);
|
|
1444
|
+
const colType = mapType(field);
|
|
1445
|
+
builder = builder.addColumn(colName, colType, (col) => {
|
|
1446
|
+
let c = col;
|
|
1447
|
+
if (field.unique)
|
|
1448
|
+
c = c.unique();
|
|
1449
|
+
if (field.required)
|
|
1450
|
+
c = c.notNull();
|
|
1451
|
+
if (field.defaultValue !== undefined)
|
|
1452
|
+
c = c.defaultTo(field.defaultValue);
|
|
1453
|
+
return c;
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
if (versions?.drafts) {
|
|
1457
|
+
builder = builder.addColumn("_status", "text", (col) => col.defaultTo("draft"));
|
|
1458
|
+
}
|
|
1459
|
+
if (hasTimestamps) {
|
|
1460
|
+
const ts = collection.timestamps ?? {};
|
|
1461
|
+
const config = typeof ts === "object" ? ts : {};
|
|
1462
|
+
const createdField = toSnakeCase(config.createdAt ?? "createdAt");
|
|
1463
|
+
const updatedField = toSnakeCase(config.updatedAt ?? "updatedAt");
|
|
1464
|
+
const tsType = dialect === "postgres" ? "timestamptz" : "text";
|
|
1465
|
+
builder = builder.addColumn(createdField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
|
|
1466
|
+
builder = builder.addColumn(updatedField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
|
|
1467
|
+
}
|
|
1468
|
+
await builder.execute();
|
|
1469
|
+
} else {
|
|
1470
|
+
const existingColSet = new Set(existingTable.columns.map((c) => c.name));
|
|
1471
|
+
for (const field of flattenedFields) {
|
|
1472
|
+
if (field.type === "relationship" && "hasMany" in field && field.hasMany)
|
|
1473
|
+
continue;
|
|
1474
|
+
const colName = toSnakeCase(field.name);
|
|
1475
|
+
if (colName === "id")
|
|
1476
|
+
continue;
|
|
1477
|
+
assertSafeIdentifier(colName);
|
|
1478
|
+
if (!existingColSet.has(colName)) {
|
|
1479
|
+
logger.info(` ${logger.format("green", "✔")} Adding column: ${logger.bold(`${tableName}.${colName}`)}`);
|
|
1480
|
+
const colType = mapType(field);
|
|
1481
|
+
await db.schema.alterTable(tableName).addColumn(colName, colType).execute();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (versions?.drafts && !existingColSet.has("_status")) {
|
|
1485
|
+
logger.info(` ${logger.format("green", "✔")} Adding status column to: ${tableName}`);
|
|
1486
|
+
await db.schema.alterTable(tableName).addColumn("_status", "text", (col) => col.defaultTo("draft")).execute();
|
|
1487
|
+
}
|
|
1488
|
+
if (hasTimestamps) {
|
|
1489
|
+
const ts = collection.timestamps ?? {};
|
|
1490
|
+
const config = typeof ts === "object" ? ts : {};
|
|
1491
|
+
const createdField = toSnakeCase(config.createdAt ?? "createdAt");
|
|
1492
|
+
const updatedField = toSnakeCase(config.updatedAt ?? "updatedAt");
|
|
1493
|
+
const tsType = dialect === "postgres" ? "timestamptz" : "text";
|
|
1494
|
+
if (!existingColSet.has(createdField)) {
|
|
1495
|
+
logger.info(` -> Adding missing timestamp column: ${tableName}.${createdField}`);
|
|
1496
|
+
await db.schema.alterTable(tableName).addColumn(createdField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
|
|
1497
|
+
}
|
|
1498
|
+
if (!existingColSet.has(updatedField)) {
|
|
1499
|
+
logger.info(` -> Adding missing timestamp column: ${tableName}.${updatedField}`);
|
|
1500
|
+
await db.schema.alterTable(tableName).addColumn(updatedField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (pushDestructive) {
|
|
1504
|
+
const expectedCols = new Set(flattenedFields.filter((f) => !(f.type === "relationship" && f.hasMany)).map((f) => toSnakeCase(f.name)));
|
|
1505
|
+
if (hasTimestamps) {
|
|
1506
|
+
const ts = collection.timestamps ?? {};
|
|
1507
|
+
const config = typeof ts === "object" ? ts : {};
|
|
1508
|
+
expectedCols.add(toSnakeCase(config.createdAt ?? "createdAt"));
|
|
1509
|
+
expectedCols.add(toSnakeCase(config.updatedAt ?? "updatedAt"));
|
|
1510
|
+
}
|
|
1511
|
+
if (versions?.drafts)
|
|
1512
|
+
expectedCols.add("_status");
|
|
1513
|
+
expectedCols.add("id");
|
|
1514
|
+
for (const existingCol of existingColSet) {
|
|
1515
|
+
const colName = existingCol;
|
|
1516
|
+
if (!expectedCols.has(colName)) {
|
|
1517
|
+
logger.info(` -> Dropping stale column: ${logger.format("red", `${tableName}.${colName}`)}`);
|
|
1518
|
+
try {
|
|
1519
|
+
await db.schema.alterTable(tableName).dropColumn(colName).execute();
|
|
1520
|
+
} catch (_) {
|
|
1521
|
+
logger.warn(`Failed to drop column ${colName}.`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const relationalFields = getRelationalFields(collection.fields);
|
|
1528
|
+
for (const field of relationalFields) {
|
|
1529
|
+
if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
|
|
1530
|
+
const colName = toSnakeCase(field.name);
|
|
1531
|
+
const joinTableName = `${tableName}_${colName}_relations`.toLowerCase();
|
|
1532
|
+
assertSafeIdentifier(joinTableName);
|
|
1533
|
+
expectedTableNames.add(joinTableName);
|
|
1534
|
+
if (!existingTableMap.has(joinTableName)) {
|
|
1535
|
+
logger.info(` -> Creating relation table: ${logger.format("green", `"${joinTableName}"`)}`);
|
|
1536
|
+
await db.schema.createTable(joinTableName).ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("source_id", "text", (col) => col.notNull()).addColumn("target_id", "text", (col) => col.notNull()).addColumn("order", "integer", (col) => col.defaultTo(0)).execute();
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
for (const field of relationalFields) {
|
|
1541
|
+
if (field.type === "blocks" && field.blocks && Array.isArray(field.blocks)) {
|
|
1542
|
+
for (const block of field.blocks) {
|
|
1543
|
+
const blockName = toSnakeCase(field.name);
|
|
1544
|
+
const blockSlug = toSnakeCase(block.slug);
|
|
1545
|
+
assertSafeIdentifier(blockName);
|
|
1546
|
+
assertSafeIdentifier(blockSlug);
|
|
1547
|
+
const blockTableName = `${tableName}_${blockName}_${blockSlug}`.toLowerCase();
|
|
1548
|
+
assertSafeIdentifier(blockTableName);
|
|
1549
|
+
expectedTableNames.add(blockTableName);
|
|
1550
|
+
const existingBlockTable = existingTableMap.get(blockTableName);
|
|
1551
|
+
if (!existingBlockTable) {
|
|
1552
|
+
logger.info(` -> Creating block table: ${logger.format("green", `"${blockTableName}"`)}`);
|
|
1553
|
+
let bBuilder = db.schema.createTable(blockTableName).ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("_parent_id", "text", (col) => col.notNull()).addColumn("_order", "integer", (col) => col.defaultTo(0)).addColumn("block_type", "text", (col) => col.notNull().defaultTo(block.slug));
|
|
1554
|
+
const blockFlattened = flattenFields(block.fields);
|
|
1555
|
+
for (const bField of blockFlattened) {
|
|
1556
|
+
const bColName = toSnakeCase(bField.name);
|
|
1557
|
+
if (bColName === "id")
|
|
1558
|
+
continue;
|
|
1559
|
+
assertSafeIdentifier(bColName);
|
|
1560
|
+
const bColType = mapType(bField);
|
|
1561
|
+
bBuilder = bBuilder.addColumn(bColName, bColType, (col) => {
|
|
1562
|
+
let c = col;
|
|
1563
|
+
if (bField.unique)
|
|
1564
|
+
c = c.unique();
|
|
1565
|
+
if (bField.required)
|
|
1566
|
+
c = c.notNull();
|
|
1567
|
+
if (bField.defaultValue !== undefined)
|
|
1568
|
+
c = c.defaultTo(bField.defaultValue);
|
|
1569
|
+
return c;
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
const tsType = dialect === "postgres" ? "timestamptz" : "text";
|
|
1573
|
+
bBuilder = bBuilder.addColumn("created_at", tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
|
|
1574
|
+
bBuilder = bBuilder.addColumn("updated_at", tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
|
|
1575
|
+
await bBuilder.execute();
|
|
1576
|
+
} else {
|
|
1577
|
+
const existingBColSet = new Set(existingBlockTable.columns.map((c) => c.name));
|
|
1578
|
+
const blockFlattened = flattenFields(block.fields);
|
|
1579
|
+
for (const bField of blockFlattened) {
|
|
1580
|
+
const bColName = toSnakeCase(bField.name);
|
|
1581
|
+
if (bColName === "id")
|
|
1582
|
+
continue;
|
|
1583
|
+
if (!existingBColSet.has(bColName)) {
|
|
1584
|
+
const bColType = mapType(bField);
|
|
1585
|
+
await db.schema.alterTable(blockTableName).addColumn(bColName, bColType).execute();
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (!existingBColSet.has("_parent_id")) {
|
|
1589
|
+
await db.schema.alterTable(blockTableName).addColumn("_parent_id", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
1590
|
+
}
|
|
1591
|
+
if (!existingBColSet.has("_order")) {
|
|
1592
|
+
await db.schema.alterTable(blockTableName).addColumn("_order", "integer", (col) => col.defaultTo(0)).execute();
|
|
1593
|
+
}
|
|
1594
|
+
if (!existingBColSet.has("block_type")) {
|
|
1595
|
+
await db.schema.alterTable(blockTableName).addColumn("block_type", "text", (col) => col.notNull().defaultTo(block.slug)).execute();
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
if (pushDestructive) {
|
|
1603
|
+
for (const tableName of existingTables) {
|
|
1604
|
+
if (!expectedTableNames.has(tableName) && !tableName.startsWith("better_auth") && !tableName.startsWith("__kysely")) {
|
|
1605
|
+
logger.info(` -> Dropping stale table: ${logger.format("red", `"${tableName}"`)}`);
|
|
1606
|
+
try {
|
|
1607
|
+
await db.schema.dropTable(tableName).ifExists().cascade().execute();
|
|
1608
|
+
} catch (_) {
|
|
1609
|
+
try {
|
|
1610
|
+
await db.schema.dropTable(tableName).ifExists().execute();
|
|
1611
|
+
} catch (_2) {
|
|
1612
|
+
logger.warn(`Failed to drop table ${tableName}`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
logger.info(`${logger.format("green", "✔")} Schema synchronization complete.`);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/db/sqlite.ts
|
|
1622
|
+
init_context();
|
|
36
1623
|
class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
37
1624
|
path;
|
|
38
1625
|
name = "sqlite";
|
|
@@ -51,9 +1638,9 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
51
1638
|
throw new Error("Database not connected. Call connect() first.");
|
|
52
1639
|
return this._db;
|
|
53
1640
|
}
|
|
54
|
-
constructor(
|
|
1641
|
+
constructor(path, options) {
|
|
55
1642
|
super();
|
|
56
|
-
this.path =
|
|
1643
|
+
this.path = path;
|
|
57
1644
|
this.push = options?.push ?? true;
|
|
58
1645
|
this.pushDestructive = options?.pushDestructive ?? false;
|
|
59
1646
|
this.migrationDir = options?.migrationDir ?? "./migrations";
|
|
@@ -61,16 +1648,16 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
61
1648
|
async connect() {
|
|
62
1649
|
if (this._db)
|
|
63
1650
|
return;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1651
|
+
let Database;
|
|
1652
|
+
try {
|
|
1653
|
+
const mod = await import("better-sqlite3");
|
|
1654
|
+
Database = mod.default || mod;
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
throw new Error("Cannot use SQLiteAdapter: better-sqlite3 is not installed or could not be loaded. If you are using Cloudflare Workers, please use D1Adapter instead.");
|
|
68
1657
|
}
|
|
69
|
-
const require2 = createRequire(moduleUrl);
|
|
70
|
-
const Database = require2("better-sqlite3");
|
|
71
1658
|
this._rawDb = new Database(this.path);
|
|
72
|
-
const { createOpacaKysely } = await
|
|
73
|
-
this._db =
|
|
1659
|
+
const { createOpacaKysely: createOpacaKysely2 } = await Promise.resolve().then(() => (init_factory(), exports_factory));
|
|
1660
|
+
this._db = createOpacaKysely2({
|
|
74
1661
|
dialect: new SqliteDialect({
|
|
75
1662
|
database: this._rawDb
|
|
76
1663
|
}),
|
|
@@ -569,11 +2156,14 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
569
2156
|
return this.findGlobal(slug);
|
|
570
2157
|
}
|
|
571
2158
|
async runMigrations() {
|
|
2159
|
+
const fs = await import("node:fs/promises");
|
|
2160
|
+
const path = await import("node:path");
|
|
2161
|
+
const { pathToFileURL } = await import("node:url");
|
|
572
2162
|
const migrator = new Migrator({
|
|
573
2163
|
db: this.db,
|
|
574
2164
|
provider: new FileMigrationProvider({
|
|
575
|
-
fs,
|
|
576
|
-
path,
|
|
2165
|
+
fs: fs.default || fs,
|
|
2166
|
+
path: path.default || path,
|
|
577
2167
|
migrationFolder: pathToFileURL(path.resolve(process.cwd(), this.migrationDir)).href
|
|
578
2168
|
})
|
|
579
2169
|
});
|
|
@@ -599,8 +2189,8 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
|
|
|
599
2189
|
}
|
|
600
2190
|
}
|
|
601
2191
|
}
|
|
602
|
-
function createSQLiteAdapter(
|
|
603
|
-
return new SQLiteAdapter(
|
|
2192
|
+
function createSQLiteAdapter(path, options) {
|
|
2193
|
+
return new SQLiteAdapter(path, options);
|
|
604
2194
|
}
|
|
605
2195
|
export {
|
|
606
2196
|
createSQLiteAdapter,
|