opacacms 0.2.1 → 0.3.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.
- package/README.md +31 -22
- package/dist/admin/auth-client.d.ts +39 -39
- package/dist/admin/index.d.ts +2 -2
- package/dist/admin/index.js +15 -10520
- package/dist/admin/plugin-client.d.ts +65 -0
- package/dist/admin/react.d.ts +2 -2
- package/dist/admin/react.js +34 -4
- package/dist/admin/stores/ui.d.ts +19 -4
- package/dist/admin/ui/components/PluginSettingsForm.d.ts +2 -2
- package/dist/admin/ui/components/custom-alert.d.ts +7 -0
- package/dist/admin/ui/components/{DetailSheet.d.ts → detail-sheet.d.ts} +1 -2
- package/dist/admin/ui/components/fields/FieldLabel.d.ts +1 -1
- package/dist/admin/ui/components/fields/RelationshipField.d.ts +1 -1
- package/dist/admin/ui/components/media/AssetManagerModal.d.ts +2 -2
- package/dist/admin/ui/components/plugin-iframe.d.ts +7 -0
- package/dist/admin/ui/components/ui/accordion.d.ts +17 -7
- package/dist/admin/ui/components/ui/alert-dialog.d.ts +16 -12
- package/dist/admin/ui/components/ui/button.d.ts +11 -7
- package/dist/admin/ui/components/ui/relationship.d.ts +1 -1
- package/dist/admin/ui/components/ui/sheet.d.ts +14 -27
- package/dist/admin/ui/components/ui/tooltip.d.ts +7 -0
- package/dist/admin/ui/components/versions-sheet.d.ts +4 -5
- package/dist/admin/ui/views/collection-list-view.d.ts +1 -1
- package/dist/admin/ui/views/dashboard-view.d.ts +1 -1
- package/dist/admin/ui/views/media-registry-view.d.ts +3 -3
- package/dist/admin/ui/views/settings-view.d.ts +2 -2
- package/dist/admin/vue.js +27 -4
- package/dist/admin/webcomponent.js +66 -16
- package/dist/admin.css +1 -1
- package/dist/auth/index.d.ts +43 -43
- package/dist/{chunk-7y1nbmw6.js → chunk-1bd7fz7n.js} +32 -2
- package/dist/chunk-1qm0m8r8.js +413 -0
- package/dist/chunk-2k3ysje3.js +31 -0
- package/dist/{chunk-jdfw4v3r.js → chunk-3j9zjfmn.js} +95 -30
- package/dist/{chunk-byq8g0rd.js → chunk-48ywpd0a.js} +16 -22
- package/dist/{chunk-tfnaf41w.js → chunk-5422w4eq.js} +41 -25
- package/dist/chunk-56n342hs.js +95 -0
- package/dist/chunk-5b8r0v8c.js +47 -0
- package/dist/chunk-63yg00vx.js +263 -0
- package/dist/{chunk-8sqjbsgt.js → chunk-6bywt602.js} +26 -1
- package/dist/{chunk-v9z61v3g.js → chunk-6qs0g65f.js} +43 -3
- package/dist/chunk-7rr5p01g.js +581 -0
- package/dist/{chunk-2es275xs.js → chunk-941zxavt.js} +867 -322
- package/dist/{chunk-51z3x7kq.js → chunk-a3qae86h.js} +1 -1
- package/dist/{chunk-3rdhbedb.js → chunk-adq2b75c.js} +2 -2
- package/dist/chunk-d0tb1xjw.js +93 -0
- package/dist/chunk-d7cgd6vn.js +318 -0
- package/dist/{chunk-6d1vdfwa.js → chunk-e0g6gn7n.js} +54 -75
- package/dist/chunk-ec4jhybj.js +1137 -0
- package/dist/chunk-fatyf6f7.js +221 -0
- package/dist/{chunk-526a3gqx.js → chunk-fnsf1dfm.js} +1 -1
- package/dist/chunk-g9bxb6h0.js +205 -0
- package/dist/chunk-gyaf5kgf.js +10 -0
- package/dist/{chunk-9kxpbcb1.js → chunk-h6dhexzr.js} +16 -7
- package/dist/{chunk-dykn5hr6.js → chunk-j8js1y0h.js} +31 -74
- package/dist/{chunk-t0zg026p.js → chunk-jq1drsen.js} +12 -1
- package/dist/{chunk-b3kr8w41.js → chunk-m24yqkeq.js} +38 -26
- package/dist/chunk-m5ems3hh.js +410 -0
- package/dist/{chunk-8scgdznr.js → chunk-m83ybzf8.js} +15 -18
- package/dist/chunk-majsbncm.js +98 -0
- package/dist/chunk-mp2gt9yh.js +237 -0
- package/dist/chunk-n1twhqmf.js +54 -0
- package/dist/{chunk-bygjkgrx.js → chunk-naqcqj8n.js} +57 -80
- package/dist/chunk-q5sb5dcr.js +15 -0
- package/dist/{chunk-06ks4ggh.js → chunk-qhdsjek6.js} +49 -89
- package/dist/{chunk-n133qpsm.js → chunk-qsh2nqz3.js} +50 -81
- package/dist/chunk-r0ms5tk1.js +76 -0
- package/dist/chunk-rwqwsanx.js +75 -0
- package/dist/chunk-sqsfk9p4.js +700 -0
- package/dist/{chunk-5gvbp2qa.js → chunk-x7bnzswh.js} +25 -18
- package/dist/cli/commands/dev.d.ts +8 -0
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/generate.d.ts +26 -0
- package/dist/cli/commands/init.d.ts +13 -1
- package/dist/cli/commands/migrate.d.ts +33 -0
- package/dist/cli/commands/plugin.d.ts +13 -0
- package/dist/cli/commands/seed.d.ts +21 -0
- package/dist/cli/{commands/migrate-commands.d.ts → core/migrations/migrate-logic.d.ts} +2 -2
- package/dist/cli/core/migrations/schema-diff-engine.d.ts +12 -0
- package/dist/cli/core/migrations/schema-diff.d.ts +11 -0
- package/dist/cli/{seeding.d.ts → core/seeding/auto-seed.d.ts} +7 -4
- package/dist/cli/core/seeding/seed-logic.d.ts +2 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +6 -170
- package/dist/client/RichText.d.ts +5 -0
- package/dist/client/rich-text-utils.d.ts +5 -0
- package/dist/client.js +3 -2
- package/dist/config.d.ts +3 -3
- package/dist/db/better-sqlite.d.ts +2 -3
- package/dist/db/better-sqlite.js +6 -5
- package/dist/db/bun-sqlite.d.ts +2 -3
- package/dist/db/bun-sqlite.js +6 -5
- package/dist/db/d1.d.ts +13 -7
- package/dist/db/d1.js +6 -5
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +10 -12
- package/dist/db/kysely/factory.d.ts +29 -0
- package/dist/db/kysely/plugins/audit-logging.d.ts +48 -0
- package/dist/db/kysely/plugins/auto-timestamps.d.ts +38 -0
- package/dist/db/kysely/plugins/cursor-pagination.d.ts +42 -0
- package/dist/db/kysely/plugins/deadlock-handler.d.ts +47 -0
- package/dist/db/kysely/plugins/draft-swapper.d.ts +33 -0
- package/dist/db/kysely/plugins/field-masking.d.ts +45 -0
- package/dist/db/kysely/plugins/fts-normalizer.d.ts +38 -0
- package/dist/db/kysely/plugins/i18n-fallback.d.ts +48 -0
- package/dist/db/kysely/plugins/id-generation.d.ts +42 -0
- package/dist/db/kysely/plugins/index.d.ts +16 -0
- package/dist/db/kysely/plugins/json-flattener.d.ts +38 -0
- package/dist/db/kysely/plugins/relationship-preloading.d.ts +39 -0
- package/dist/db/kysely/plugins/slug-generation.d.ts +37 -0
- package/dist/db/kysely/plugins/soft-delete.d.ts +42 -0
- package/dist/db/kysely/plugins/tree-resolver.d.ts +39 -0
- package/dist/db/kysely/plugins/virtual-field-resolver.d.ts +54 -0
- package/dist/db/kysely/plugins/zod-coercion.d.ts +34 -0
- package/dist/db/kysely/snapshot/snapshot-manager.d.ts +18 -0
- package/dist/db/postgres.d.ts +2 -2
- package/dist/db/postgres.js +6 -5
- package/dist/db/sqlite.d.ts +2 -3
- package/dist/db/sqlite.js +6 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -7
- package/dist/runtimes/bun.js +9 -6
- package/dist/runtimes/cloudflare-workers.d.ts +3 -1
- package/dist/runtimes/cloudflare-workers.js +36 -7
- package/dist/runtimes/next.js +8 -5
- package/dist/runtimes/node.js +9 -6
- package/dist/schema/collection.d.ts +116 -70
- package/dist/schema/compiler.d.ts +6 -0
- package/dist/schema/global.d.ts +38 -71
- package/dist/schema/index.d.ts +5 -4
- package/dist/schema/index.js +35 -550
- package/dist/schema/zod.d.ts +564 -0
- package/dist/server/admin-router.d.ts +1 -1
- package/dist/server/collection-router.d.ts +1 -1
- package/dist/server/graphql.d.ts +6 -0
- package/dist/server/handlers.d.ts +25 -7
- package/dist/server/middlewares/auth.d.ts +1 -1
- package/dist/server/plugins-loader.d.ts +1 -1
- package/dist/server/router.d.ts +2 -2
- package/dist/server/routers/admin.d.ts +1 -1
- package/dist/server/routers/auth.d.ts +1 -1
- package/dist/server/routers/collections.d.ts +4 -1
- package/dist/server/routers/plugins.d.ts +2 -2
- package/dist/server/setup-middlewares.d.ts +1 -1
- package/dist/server/system-router.d.ts +1 -1
- package/dist/server.js +11 -6
- package/dist/storage/adapters/cloudflare-r2.d.ts +11 -2
- package/dist/storage/index.js +5 -5
- package/dist/types.d.ts +253 -42
- package/dist/utils/context.d.ts +14 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/string.d.ts +10 -0
- package/dist/utils/webhooks-engine.d.ts +24 -0
- package/dist/validation.d.ts +67 -1
- package/dist/validator.d.ts +1 -0
- package/package.json +50 -11
- package/src/cli/index.ts +117 -0
- package/dist/chunk-6qq3ne6b.js +0 -288
- package/dist/chunk-6v1fw7q7.js +0 -126
- package/dist/chunk-7a9kn0np.js +0 -116
- package/dist/chunk-bexcv7xe.js +0 -36
- package/dist/chunk-d3ffeqp9.js +0 -87
- package/dist/chunk-fj19qccp.js +0 -78
- package/dist/chunk-g1jb60xd.js +0 -17
- package/dist/chunk-j53pz21t.js +0 -20
- package/dist/chunk-mkn49zmy.js +0 -102
- package/dist/chunk-r39em4yj.js +0 -29
- package/dist/chunk-rsf0tpy1.js +0 -8
- package/dist/chunk-srsac177.js +0 -85
- package/dist/chunk-twpvxfce.js +0 -64
- package/dist/chunk-ywm4t2gm.js +0 -19
- package/dist/cli/commands/plugin-sync.d.ts +0 -1
- package/dist/cli/commands/seed-command.d.ts +0 -2
- package/dist/plugins/ui-bridge.d.ts +0 -12
- package/dist/schema/fields/base.d.ts +0 -84
- package/dist/schema/fields/index.d.ts +0 -147
- package/dist/schema/infer.d.ts +0 -55
- /package/dist/admin/ui/components/{ColumnVisibilityToggle.d.ts → column-visibility-toggle.d.ts} +0 -0
- /package/dist/admin/ui/components/{DataDetailView.d.ts → data-detail-view.d.ts} +0 -0
- /package/dist/cli/{d1-mock.d.ts → core/mocks/d1-mock.d.ts} +0 -0
- /package/dist/cli/{r2-mock.d.ts → core/mocks/r2-mock.d.ts} +0 -0
- /package/dist/cli/{commands → core/plugins}/plugin-build.d.ts +0 -0
- /package/dist/cli/{commands → core/plugins}/plugin-init.d.ts +0 -0
- /package/dist/cli/{commands → core/types}/generate-types.d.ts +0 -0
- /package/dist/{schema/fields/validation.test.d.ts → cli/seeding.test.d.ts} +0 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPreviousDataFromContext,
|
|
3
|
+
getUserIdFromContext
|
|
4
|
+
} from "./chunk-q5sb5dcr.js";
|
|
5
|
+
import {
|
|
6
|
+
toSnakeCase
|
|
7
|
+
} from "./chunk-qxt9vge8.js";
|
|
8
|
+
import"./chunk-6bywt602.js";
|
|
9
|
+
|
|
10
|
+
// src/db/kysely/factory.ts
|
|
11
|
+
import { CamelCasePlugin, Kysely } from "kysely";
|
|
12
|
+
|
|
13
|
+
// src/db/kysely/plugins/audit-logging.ts
|
|
14
|
+
class AuditLoggingPlugin {
|
|
15
|
+
auditTable;
|
|
16
|
+
getUserId;
|
|
17
|
+
db;
|
|
18
|
+
queryNodes = new WeakMap;
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.auditTable = config.auditTable || "_audit_logs";
|
|
21
|
+
this.getUserId = config.getUserId;
|
|
22
|
+
}
|
|
23
|
+
setDb(db) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
}
|
|
26
|
+
transformQuery(args) {
|
|
27
|
+
this.queryNodes.set(args.queryId, args.node);
|
|
28
|
+
return args.node;
|
|
29
|
+
}
|
|
30
|
+
async transformResult(args) {
|
|
31
|
+
const { result, queryId } = args;
|
|
32
|
+
const queryNode = this.queryNodes.get(queryId);
|
|
33
|
+
this.queryNodes.delete(queryId);
|
|
34
|
+
if (!queryNode || queryNode.kind === "SelectQueryNode")
|
|
35
|
+
return result;
|
|
36
|
+
let tableName = "";
|
|
37
|
+
let operation = "other";
|
|
38
|
+
if (queryNode.kind === "InsertQueryNode") {
|
|
39
|
+
tableName = this.getTableName(queryNode.into);
|
|
40
|
+
operation = "create";
|
|
41
|
+
} else if (queryNode.kind === "UpdateQueryNode") {
|
|
42
|
+
tableName = this.getTableName(queryNode.table);
|
|
43
|
+
operation = "update";
|
|
44
|
+
} else if (queryNode.kind === "DeleteQueryNode") {
|
|
45
|
+
tableName = this.getTableName(queryNode.table || queryNode.from?.froms?.[0]);
|
|
46
|
+
operation = "delete";
|
|
47
|
+
}
|
|
48
|
+
if (!tableName || tableName === this.auditTable)
|
|
49
|
+
return result;
|
|
50
|
+
const stashedPrevious = getPreviousDataFromContext();
|
|
51
|
+
this.logOperation(operation, tableName, queryNode, result, stashedPrevious).catch((err) => {
|
|
52
|
+
console.error("[OpacaCMS] Audit logging failed:", err);
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
getTableName(node) {
|
|
57
|
+
if (!node)
|
|
58
|
+
return "";
|
|
59
|
+
if (node.kind === "TableNode") {
|
|
60
|
+
return node.table?.identifier?.name || node.table?.name || "";
|
|
61
|
+
}
|
|
62
|
+
if (node.table?.kind === "TableNode") {
|
|
63
|
+
return node.table.table?.identifier?.name || node.table.table?.name || "";
|
|
64
|
+
}
|
|
65
|
+
return node.table?.name || node.table?.identifier?.name || node.name || "";
|
|
66
|
+
}
|
|
67
|
+
async logOperation(operation, collection, queryNode, result, stashedPrevious = null) {
|
|
68
|
+
if (!this.db) {
|
|
69
|
+
console.warn("[OpacaCMS] AuditLoggingPlugin: No database instance set, skipping log.");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
let entityId = "unknown";
|
|
74
|
+
let newData = null;
|
|
75
|
+
let previousData = null;
|
|
76
|
+
if (queryNode.kind === "InsertQueryNode") {
|
|
77
|
+
entityId = result.insertId?.toString() || "new";
|
|
78
|
+
const columns = queryNode.columns;
|
|
79
|
+
const valuesNode = queryNode.values;
|
|
80
|
+
if (Array.isArray(columns) && valuesNode?.kind === "ValuesNode") {
|
|
81
|
+
const row = valuesNode.values?.[0]?.values;
|
|
82
|
+
if (Array.isArray(row)) {
|
|
83
|
+
newData = {};
|
|
84
|
+
for (let i = 0;i < columns.length; i++) {
|
|
85
|
+
const col = columns[i]?.column?.name || columns[i]?.name;
|
|
86
|
+
if (col) {
|
|
87
|
+
const node = row[i];
|
|
88
|
+
newData[col] = node?.value !== undefined ? node.value : node?.name || node?.identifier?.name || null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if ((entityId === "new" || entityId === "unknown") && stashedPrevious?.id) {
|
|
94
|
+
entityId = stashedPrevious.id.toString();
|
|
95
|
+
}
|
|
96
|
+
} else if (queryNode.kind === "UpdateQueryNode") {
|
|
97
|
+
entityId = "updated";
|
|
98
|
+
const where = queryNode.where;
|
|
99
|
+
const filter = where?.where;
|
|
100
|
+
if (filter?.kind === "BinaryOperationNode") {
|
|
101
|
+
const op = filter;
|
|
102
|
+
const leftNode = op.left;
|
|
103
|
+
const leftName = leftNode?.column?.name || leftNode?.name || leftNode?.identifier?.name;
|
|
104
|
+
if (op.operator?.operator === "=" && leftName === "id") {
|
|
105
|
+
const right = op.right;
|
|
106
|
+
entityId = (right?.value !== undefined ? right.value : right?.name || right?.identifier?.name)?.toString() || "updated";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (entityId === "updated" && stashedPrevious?.id) {
|
|
110
|
+
entityId = stashedPrevious.id.toString();
|
|
111
|
+
}
|
|
112
|
+
const updates = queryNode.updates;
|
|
113
|
+
if (Array.isArray(updates)) {
|
|
114
|
+
newData = {};
|
|
115
|
+
for (const upd of updates) {
|
|
116
|
+
const col = upd.column?.column?.name || upd.column?.name;
|
|
117
|
+
if (col) {
|
|
118
|
+
const valNode = upd.value;
|
|
119
|
+
newData[col] = valNode?.value !== undefined ? valNode.value : valNode?.name || valNode?.identifier?.name || null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (entityId !== "updated" && entityId !== "unknown") {
|
|
124
|
+
previousData = stashedPrevious;
|
|
125
|
+
if (previousData) {
|
|
126
|
+
console.debug(`[AuditLoggingPlugin] Used stashed previous data for ${entityId}`);
|
|
127
|
+
}
|
|
128
|
+
if (!previousData) {
|
|
129
|
+
previousData = getPreviousDataFromContext();
|
|
130
|
+
}
|
|
131
|
+
if (!previousData) {
|
|
132
|
+
try {
|
|
133
|
+
previousData = await this.db.selectFrom(collection).selectAll().where("id", "=", entityId).executeTakeFirst();
|
|
134
|
+
if (previousData) {
|
|
135
|
+
console.debug(`[AuditLoggingPlugin] Fallback SELECT fetched data for ${entityId}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else if (queryNode.kind === "DeleteQueryNode") {
|
|
141
|
+
entityId = "deleted";
|
|
142
|
+
}
|
|
143
|
+
await this.db.insertInto(this.auditTable).values({
|
|
144
|
+
id: crypto.randomUUID(),
|
|
145
|
+
operation,
|
|
146
|
+
collection,
|
|
147
|
+
entity_id: entityId,
|
|
148
|
+
user_id: (this.getUserId ? this.getUserId() : null) || getUserIdFromContext(),
|
|
149
|
+
previous_data: previousData ? JSON.stringify(previousData) : null,
|
|
150
|
+
new_data: newData ? JSON.stringify(newData) : "{}",
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
created_at: new Date().toISOString(),
|
|
153
|
+
updated_at: new Date().toISOString()
|
|
154
|
+
}).execute();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error("[OpacaCMS] Failed to write audit log:", err);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/db/kysely/plugins/auto-timestamps.ts
|
|
162
|
+
class AutoTimestampsPlugin {
|
|
163
|
+
createdAtColumn;
|
|
164
|
+
updatedAtColumn;
|
|
165
|
+
constructor(config = {}) {
|
|
166
|
+
this.createdAtColumn = config.createdAtColumn || "created_at";
|
|
167
|
+
this.updatedAtColumn = config.updatedAtColumn || "updated_at";
|
|
168
|
+
}
|
|
169
|
+
transformQuery(args) {
|
|
170
|
+
const { node } = args;
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
if (node.kind === "InsertQueryNode") {} else if (node.kind === "UpdateQueryNode") {}
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
async transformResult(args) {
|
|
176
|
+
return args.result;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/db/kysely/plugins/cursor-pagination.ts
|
|
181
|
+
class CursorPaginationPlugin {
|
|
182
|
+
after;
|
|
183
|
+
cursorColumn;
|
|
184
|
+
constructor(config) {
|
|
185
|
+
this.after = config.after;
|
|
186
|
+
this.cursorColumn = config.cursorColumn || "id";
|
|
187
|
+
}
|
|
188
|
+
transformQuery(args) {
|
|
189
|
+
if (this.after === undefined)
|
|
190
|
+
return args.node;
|
|
191
|
+
const { node } = args;
|
|
192
|
+
if (node.kind === "SelectQueryNode") {}
|
|
193
|
+
return node;
|
|
194
|
+
}
|
|
195
|
+
async transformResult(args) {
|
|
196
|
+
return args.result;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/db/kysely/plugins/deadlock-handler.ts
|
|
201
|
+
class DeadlockRetryPlugin {
|
|
202
|
+
maxRetries;
|
|
203
|
+
initialDelay;
|
|
204
|
+
constructor(config) {
|
|
205
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
206
|
+
this.initialDelay = config.initialDelay ?? 50;
|
|
207
|
+
}
|
|
208
|
+
transformQuery(args) {
|
|
209
|
+
return args.node;
|
|
210
|
+
}
|
|
211
|
+
async transformResult(args) {
|
|
212
|
+
return args.result;
|
|
213
|
+
}
|
|
214
|
+
async retry(fn) {
|
|
215
|
+
let lastError;
|
|
216
|
+
for (let attempt = 0;attempt <= this.maxRetries; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
return await fn();
|
|
219
|
+
} catch (error) {
|
|
220
|
+
lastError = error;
|
|
221
|
+
const isLockError = error?.message?.includes("SQLITE_BUSY") || error?.message?.includes("database is locked") || error?.code === "SQLITE_BUSY";
|
|
222
|
+
if (!isLockError || attempt >= this.maxRetries) {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
const delay = this.initialDelay * 2 ** attempt;
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw lastError;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/db/kysely/plugins/draft-swapper.ts
|
|
234
|
+
class DraftSwapperPlugin {
|
|
235
|
+
draftMode;
|
|
236
|
+
tables;
|
|
237
|
+
constructor(config) {
|
|
238
|
+
this.draftMode = config.draftMode ?? false;
|
|
239
|
+
const allSlugs = [
|
|
240
|
+
...config.collections?.map((c) => c.slug) || [],
|
|
241
|
+
...config.globals?.map((g) => g.slug) || []
|
|
242
|
+
];
|
|
243
|
+
this.tables = new Set(allSlugs.map((s) => toSnakeCase(s)));
|
|
244
|
+
}
|
|
245
|
+
transformQuery(args) {
|
|
246
|
+
if (!this.draftMode)
|
|
247
|
+
return args.node;
|
|
248
|
+
const { node } = args;
|
|
249
|
+
if (node.kind === "SelectQueryNode") {
|
|
250
|
+
if (node.from && node.from.froms) {
|
|
251
|
+
let shouldSwap = false;
|
|
252
|
+
let originalTable = "";
|
|
253
|
+
const newFroms = node.from.froms.map((f) => {
|
|
254
|
+
if (f.kind === "TableNode" && f.table && f.table.kind === "IdentifierNode") {
|
|
255
|
+
const tableName = f.table.name;
|
|
256
|
+
if (this.tables?.has(tableName)) {
|
|
257
|
+
shouldSwap = true;
|
|
258
|
+
originalTable = tableName;
|
|
259
|
+
return {
|
|
260
|
+
...f,
|
|
261
|
+
table: {
|
|
262
|
+
...f.table,
|
|
263
|
+
name: "_doc_versions"
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return f;
|
|
269
|
+
});
|
|
270
|
+
if (shouldSwap) {
|
|
271
|
+
node.from.froms = newFroms;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return node;
|
|
276
|
+
}
|
|
277
|
+
async transformResult(args) {
|
|
278
|
+
const { result } = args;
|
|
279
|
+
if (!this.draftMode || result.rows.length === 0)
|
|
280
|
+
return result;
|
|
281
|
+
if (result.rows[0] && "data" in result.rows[0] && "collection" in result.rows[0]) {
|
|
282
|
+
const mappedRows = result.rows.map((row) => {
|
|
283
|
+
try {
|
|
284
|
+
const parsedData = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
|
|
285
|
+
return {
|
|
286
|
+
...parsedData,
|
|
287
|
+
_version_id: row.id,
|
|
288
|
+
_version_status: row.status,
|
|
289
|
+
_version_createdAt: row.createdAt
|
|
290
|
+
};
|
|
291
|
+
} catch {
|
|
292
|
+
return row;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return { ...result, rows: mappedRows };
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/db/kysely/plugins/fts-normalizer.ts
|
|
302
|
+
class FtsNormalizerPlugin {
|
|
303
|
+
dialect;
|
|
304
|
+
constructor(config) {
|
|
305
|
+
this.dialect = config.dialect;
|
|
306
|
+
}
|
|
307
|
+
transformQuery(args) {
|
|
308
|
+
const { node } = args;
|
|
309
|
+
return node;
|
|
310
|
+
}
|
|
311
|
+
async transformResult(args) {
|
|
312
|
+
return args.result;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/db/kysely/plugins/i18n-fallback.ts
|
|
317
|
+
class AutoTranslationFallbackPlugin {
|
|
318
|
+
localizedFields = new Map;
|
|
319
|
+
config;
|
|
320
|
+
constructor(config) {
|
|
321
|
+
this.config = config;
|
|
322
|
+
this.parseConfig(config);
|
|
323
|
+
}
|
|
324
|
+
parseConfig(config) {
|
|
325
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
326
|
+
for (const res of allResources) {
|
|
327
|
+
const tableName = toSnakeCase(res.slug);
|
|
328
|
+
const fields = new Set;
|
|
329
|
+
const findLocalizedFields = (fieldList) => {
|
|
330
|
+
for (const f of fieldList) {
|
|
331
|
+
if (f.name && f.localized) {
|
|
332
|
+
fields.add(toSnakeCase(f.name));
|
|
333
|
+
}
|
|
334
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
335
|
+
findLocalizedFields(f.fields);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
findLocalizedFields(res.fields);
|
|
340
|
+
if (fields.size > 0) {
|
|
341
|
+
this.localizedFields.set(tableName, fields);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
transformQuery(args) {
|
|
346
|
+
return args.node;
|
|
347
|
+
}
|
|
348
|
+
async transformResult(args) {
|
|
349
|
+
const { result } = args;
|
|
350
|
+
if (this.config.defaultLocale === this.config.currentLocale)
|
|
351
|
+
return result;
|
|
352
|
+
return {
|
|
353
|
+
...result,
|
|
354
|
+
rows: result.rows.map((row) => this.fallbackRow(row))
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
fallbackRow(row) {
|
|
358
|
+
if (!row)
|
|
359
|
+
return row;
|
|
360
|
+
const newRow = { ...row };
|
|
361
|
+
const currentLocSuffix = `_${this.config.currentLocale}`;
|
|
362
|
+
const defaultLocSuffix = `_${this.config.defaultLocale}`;
|
|
363
|
+
for (const [key, value] of Object.entries(newRow)) {
|
|
364
|
+
if (key.endsWith(currentLocSuffix)) {
|
|
365
|
+
const baseName = key.slice(0, -currentLocSuffix.length);
|
|
366
|
+
const defaultValueKey = `${baseName}${defaultLocSuffix}`;
|
|
367
|
+
if ((value === null || value === "" || value === undefined) && newRow[defaultValueKey] !== undefined) {
|
|
368
|
+
newRow[key] = newRow[defaultValueKey];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return newRow;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/db/kysely/plugins/id-generation.ts
|
|
377
|
+
class IdGenerationPlugin {
|
|
378
|
+
generateId;
|
|
379
|
+
idColumn;
|
|
380
|
+
constructor(config = {}) {
|
|
381
|
+
this.generateId = config.generateId || (() => crypto.randomUUID());
|
|
382
|
+
this.idColumn = config.idColumn || "id";
|
|
383
|
+
}
|
|
384
|
+
transformQuery(args) {
|
|
385
|
+
const { node } = args;
|
|
386
|
+
if (node.kind === "InsertQueryNode") {}
|
|
387
|
+
return node;
|
|
388
|
+
}
|
|
389
|
+
async transformResult(args) {
|
|
390
|
+
return args.result;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/db/kysely/plugins/json-flattener.ts
|
|
395
|
+
class JsonFlattenerPlugin {
|
|
396
|
+
jsonFields = new Map;
|
|
397
|
+
constructor(config) {
|
|
398
|
+
this.parseConfig(config);
|
|
399
|
+
}
|
|
400
|
+
parseConfig(config) {
|
|
401
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
402
|
+
for (const res of allResources) {
|
|
403
|
+
const tableName = toSnakeCase(res.slug);
|
|
404
|
+
const fields = new Set;
|
|
405
|
+
const findJsonFields = (fieldList) => {
|
|
406
|
+
for (const f of fieldList) {
|
|
407
|
+
if (f.name && (["richtext", "json", "file", "blocks"].includes(f.type) || f.localized)) {
|
|
408
|
+
fields.add(toSnakeCase(f.name));
|
|
409
|
+
}
|
|
410
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
411
|
+
findJsonFields(f.fields);
|
|
412
|
+
}
|
|
413
|
+
if ("blocks" in f && Array.isArray(f.blocks)) {
|
|
414
|
+
for (const b of f.blocks) {
|
|
415
|
+
if (b.fields)
|
|
416
|
+
findJsonFields(b.fields);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if ("tabs" in f && Array.isArray(f.tabs)) {
|
|
420
|
+
for (const t of f.tabs) {
|
|
421
|
+
if (t.fields)
|
|
422
|
+
findJsonFields(t.fields);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
findJsonFields(res.fields);
|
|
428
|
+
if (fields.size > 0) {
|
|
429
|
+
this.jsonFields.set(tableName, fields);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
transformQuery(args) {
|
|
434
|
+
const { node } = args;
|
|
435
|
+
if (node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode") {}
|
|
436
|
+
return node;
|
|
437
|
+
}
|
|
438
|
+
async transformResult(args) {
|
|
439
|
+
const { result } = args;
|
|
440
|
+
return {
|
|
441
|
+
...result,
|
|
442
|
+
rows: result.rows.map((row) => this.mapRow(row))
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
mapRow(row) {
|
|
446
|
+
if (!row)
|
|
447
|
+
return row;
|
|
448
|
+
const newRow = { ...row };
|
|
449
|
+
const isSystemTable = Object.keys(newRow).some((k) => k.startsWith("_"));
|
|
450
|
+
if (isSystemTable)
|
|
451
|
+
return newRow;
|
|
452
|
+
for (const [key, value] of Object.entries(newRow)) {
|
|
453
|
+
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
|
|
454
|
+
try {
|
|
455
|
+
newRow[key] = JSON.parse(value);
|
|
456
|
+
} catch (_e) {}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return newRow;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/db/kysely/plugins/relationship-preloading.ts
|
|
464
|
+
class RelationshipPreloadingPlugin {
|
|
465
|
+
maxDepth;
|
|
466
|
+
constructor(config = {}) {
|
|
467
|
+
this.maxDepth = config.maxDepth ?? 2;
|
|
468
|
+
}
|
|
469
|
+
transformQuery(args) {
|
|
470
|
+
return args.node;
|
|
471
|
+
}
|
|
472
|
+
async transformResult(args) {
|
|
473
|
+
const { result } = args;
|
|
474
|
+
if (result.rows.length === 0)
|
|
475
|
+
return result;
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/db/kysely/plugins/slug-generation.ts
|
|
481
|
+
class SlugGenerationPlugin {
|
|
482
|
+
sourceColumn;
|
|
483
|
+
slugColumn;
|
|
484
|
+
constructor(config = {}) {
|
|
485
|
+
this.sourceColumn = config.sourceColumn || "title";
|
|
486
|
+
this.slugColumn = config.slugColumn || "slug";
|
|
487
|
+
}
|
|
488
|
+
transformQuery(args) {
|
|
489
|
+
const { node } = args;
|
|
490
|
+
if (node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode") {}
|
|
491
|
+
return node;
|
|
492
|
+
}
|
|
493
|
+
async transformResult(args) {
|
|
494
|
+
return args.result;
|
|
495
|
+
}
|
|
496
|
+
slugify(text) {
|
|
497
|
+
return text.toString().toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/db/kysely/plugins/soft-delete.ts
|
|
502
|
+
class SoftDeletePlugin {
|
|
503
|
+
deletedAtColumn;
|
|
504
|
+
tables;
|
|
505
|
+
constructor(config = {}) {
|
|
506
|
+
this.deletedAtColumn = config.deletedAtColumn || "deleted_at";
|
|
507
|
+
this.tables = config.tables ? new Set(config.tables) : null;
|
|
508
|
+
}
|
|
509
|
+
transformQuery(args) {
|
|
510
|
+
const { node } = args;
|
|
511
|
+
if (node.kind === "DeleteQueryNode") {} else if (node.kind === "SelectQueryNode") {}
|
|
512
|
+
return node;
|
|
513
|
+
}
|
|
514
|
+
async transformResult(args) {
|
|
515
|
+
return args.result;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/db/kysely/plugins/tree-resolver.ts
|
|
520
|
+
class TreeResolverPlugin {
|
|
521
|
+
parentColumn;
|
|
522
|
+
childrenProperty;
|
|
523
|
+
constructor(config) {
|
|
524
|
+
this.parentColumn = config.parentColumn || "parent_id";
|
|
525
|
+
this.childrenProperty = config.childrenProperty || "children";
|
|
526
|
+
}
|
|
527
|
+
transformQuery(args) {
|
|
528
|
+
return args.node;
|
|
529
|
+
}
|
|
530
|
+
async transformResult(args) {
|
|
531
|
+
const { result } = args;
|
|
532
|
+
if (result.rows.length === 0)
|
|
533
|
+
return result;
|
|
534
|
+
return {
|
|
535
|
+
...result,
|
|
536
|
+
rows: this.buildTree(result.rows)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
buildTree(rows) {
|
|
540
|
+
const map = new Map;
|
|
541
|
+
const roots = [];
|
|
542
|
+
for (const row of rows) {
|
|
543
|
+
map.set(row.id, { ...row, [this.childrenProperty]: [] });
|
|
544
|
+
}
|
|
545
|
+
for (const row of rows) {
|
|
546
|
+
const parentId = row[this.parentColumn];
|
|
547
|
+
const item = map.get(row.id);
|
|
548
|
+
if (parentId && map.has(parentId)) {
|
|
549
|
+
map.get(parentId)[this.childrenProperty].push(item);
|
|
550
|
+
} else {
|
|
551
|
+
roots.push(item);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return roots;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/db/kysely/plugins/virtual-field-resolver.ts
|
|
559
|
+
class VirtualFieldResolverPlugin {
|
|
560
|
+
resolvers = new Map;
|
|
561
|
+
config;
|
|
562
|
+
constructor(config) {
|
|
563
|
+
this.config = config;
|
|
564
|
+
this.parseConfig(config);
|
|
565
|
+
}
|
|
566
|
+
parseConfig(config) {
|
|
567
|
+
const allResources = [...config.collections, ...config.globals || []];
|
|
568
|
+
for (const res of allResources) {
|
|
569
|
+
const tableName = toSnakeCase(res.slug);
|
|
570
|
+
const resResolvers = new Map;
|
|
571
|
+
const findResolvers = (fieldList) => {
|
|
572
|
+
for (const f of fieldList) {
|
|
573
|
+
if (f.name && f.type === "virtual" && typeof f.resolve === "function") {
|
|
574
|
+
resResolvers.set(f.name, f.resolve);
|
|
575
|
+
} else if (f.name && f.resolve && typeof f.resolve === "function") {
|
|
576
|
+
resResolvers.set(f.name, f.resolve);
|
|
577
|
+
}
|
|
578
|
+
if ("fields" in f && Array.isArray(f.fields)) {
|
|
579
|
+
findResolvers(f.fields);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
findResolvers(res.fields);
|
|
584
|
+
if (resResolvers.size > 0) {
|
|
585
|
+
this.resolvers.set(tableName, resResolvers);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
transformQuery(args) {
|
|
590
|
+
return args.node;
|
|
591
|
+
}
|
|
592
|
+
async transformResult(args) {
|
|
593
|
+
const { result } = args;
|
|
594
|
+
if (result.rows.length === 0)
|
|
595
|
+
return result;
|
|
596
|
+
const rows = await Promise.all(result.rows.map(async (row) => {
|
|
597
|
+
const newRow = { ...row };
|
|
598
|
+
for (const [tableName, tableResolvers] of this.resolvers.entries()) {
|
|
599
|
+
for (const [fieldName, resolveFn] of tableResolvers.entries()) {
|
|
600
|
+
try {
|
|
601
|
+
newRow[fieldName] = await resolveFn({
|
|
602
|
+
data: row,
|
|
603
|
+
req: this.config.req,
|
|
604
|
+
user: this.config.user,
|
|
605
|
+
session: this.config.session,
|
|
606
|
+
apiKey: this.config.apiKey
|
|
607
|
+
});
|
|
608
|
+
} catch (e) {
|
|
609
|
+
console.error(`Error resolving virtual field ${fieldName} for table ${tableName}:`, e);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return newRow;
|
|
614
|
+
}));
|
|
615
|
+
return {
|
|
616
|
+
...result,
|
|
617
|
+
rows
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/db/kysely/plugins/zod-coercion.ts
|
|
623
|
+
class ZodCoercionPlugin {
|
|
624
|
+
schemas;
|
|
625
|
+
constructor(config) {
|
|
626
|
+
this.schemas = config.schemas;
|
|
627
|
+
}
|
|
628
|
+
transformQuery(args) {
|
|
629
|
+
return args.node;
|
|
630
|
+
}
|
|
631
|
+
async transformResult(args) {
|
|
632
|
+
const { result } = args;
|
|
633
|
+
if (result.rows.length === 0)
|
|
634
|
+
return result;
|
|
635
|
+
const coercedRows = result.rows.map((row) => {
|
|
636
|
+
return row;
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
...result,
|
|
640
|
+
rows: coercedRows
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/db/kysely/factory.ts
|
|
646
|
+
function createOpacaKysely(options) {
|
|
647
|
+
const { dialect, config, isAdmin = false } = options;
|
|
648
|
+
const plugins = [
|
|
649
|
+
new JsonFlattenerPlugin({
|
|
650
|
+
collections: config.collections,
|
|
651
|
+
globals: config.globals
|
|
652
|
+
}),
|
|
653
|
+
new ZodCoercionPlugin({
|
|
654
|
+
schemas: {}
|
|
655
|
+
}),
|
|
656
|
+
new AutoTimestampsPlugin,
|
|
657
|
+
new IdGenerationPlugin,
|
|
658
|
+
new SlugGenerationPlugin,
|
|
659
|
+
new AuditLoggingPlugin({
|
|
660
|
+
auditTable: "_audit_logs",
|
|
661
|
+
getUserId: options.getUserId
|
|
662
|
+
}),
|
|
663
|
+
new SoftDeletePlugin,
|
|
664
|
+
new DraftSwapperPlugin({
|
|
665
|
+
draftMode: false,
|
|
666
|
+
collections: config.collections,
|
|
667
|
+
globals: config.globals
|
|
668
|
+
}),
|
|
669
|
+
new AutoTranslationFallbackPlugin({
|
|
670
|
+
collections: config.collections,
|
|
671
|
+
globals: config.globals,
|
|
672
|
+
defaultLocale: config.i18n?.defaultLocale || "en",
|
|
673
|
+
currentLocale: config.i18n?.defaultLocale || "en"
|
|
674
|
+
}),
|
|
675
|
+
new CursorPaginationPlugin({}),
|
|
676
|
+
new FtsNormalizerPlugin({
|
|
677
|
+
dialect: config.db.name
|
|
678
|
+
}),
|
|
679
|
+
new TreeResolverPlugin({}),
|
|
680
|
+
new VirtualFieldResolverPlugin({
|
|
681
|
+
collections: config.collections,
|
|
682
|
+
globals: config.globals
|
|
683
|
+
}),
|
|
684
|
+
new RelationshipPreloadingPlugin({}),
|
|
685
|
+
new DeadlockRetryPlugin({ maxRetries: 5 })
|
|
686
|
+
];
|
|
687
|
+
const db = new Kysely({
|
|
688
|
+
dialect,
|
|
689
|
+
plugins: [new CamelCasePlugin, ...plugins]
|
|
690
|
+
});
|
|
691
|
+
for (const plugin of plugins) {
|
|
692
|
+
if (plugin && typeof plugin.setDb === "function") {
|
|
693
|
+
plugin.setDb(db);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return db;
|
|
697
|
+
}
|
|
698
|
+
export {
|
|
699
|
+
createOpacaKysely
|
|
700
|
+
};
|