reflectdb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1260 -0
- package/dist/cjs/client/index.cjs +1252 -0
- package/dist/cjs/client/index.d.cts +629 -0
- package/dist/cjs/client/storage/indexeddb.cjs +253 -0
- package/dist/cjs/client/storage/indexeddb.d.cts +85 -0
- package/dist/cjs/core/index.cjs +202 -0
- package/dist/cjs/core/index.d.cts +473 -0
- package/dist/cjs/react/index.cjs +1512 -0
- package/dist/cjs/react/index.d.cts +748 -0
- package/dist/cjs/server/drizzle.cjs +413 -0
- package/dist/cjs/server/drizzle.d.cts +233 -0
- package/dist/cjs/server/index.cjs +4486 -0
- package/dist/cjs/server/index.d.cts +1988 -0
- package/dist/cjs/svelte/index.cjs +1414 -0
- package/dist/cjs/svelte/index.d.cts +645 -0
- package/dist/cjs/transport/bun-ws.cjs +244 -0
- package/dist/cjs/transport/bun-ws.d.cts +215 -0
- package/dist/cjs/transport/polling.cjs +285 -0
- package/dist/cjs/transport/polling.d.cts +210 -0
- package/dist/cjs/transport/sse.cjs +312 -0
- package/dist/cjs/transport/sse.d.cts +205 -0
- package/dist/cjs/transport/ws.cjs +330 -0
- package/dist/cjs/transport/ws.d.cts +236 -0
- package/dist/cjs/vanilla/index.cjs +1430 -0
- package/dist/cjs/vanilla/index.d.cts +657 -0
- package/dist/client/index.d.ts +629 -0
- package/dist/client/index.js +106 -0
- package/dist/client/storage/indexeddb.d.ts +85 -0
- package/dist/client/storage/indexeddb.js +213 -0
- package/dist/core/index.d.ts +473 -0
- package/dist/core/index.js +56 -0
- package/dist/react/index.d.ts +748 -0
- package/dist/react/index.js +366 -0
- package/dist/server/drizzle.d.ts +233 -0
- package/dist/server/drizzle.js +9 -0
- package/dist/server/index.d.ts +1988 -0
- package/dist/server/index.js +3959 -0
- package/dist/shared/esm-3tkwvysa.js +54 -0
- package/dist/shared/esm-b7xs9cde.js +4 -0
- package/dist/shared/esm-rw7jjtrv.js +58 -0
- package/dist/shared/esm-wkwx6bd9.js +25 -0
- package/dist/shared/esm-ytrd3hbq.js +1007 -0
- package/dist/shared/esm-z1xse19c.js +369 -0
- package/dist/svelte/index.d.ts +645 -0
- package/dist/svelte/index.js +262 -0
- package/dist/transport/bun-ws.d.ts +215 -0
- package/dist/transport/bun-ws.js +140 -0
- package/dist/transport/polling.d.ts +210 -0
- package/dist/transport/polling.js +181 -0
- package/dist/transport/sse.d.ts +205 -0
- package/dist/transport/sse.js +208 -0
- package/dist/transport/ws.d.ts +236 -0
- package/dist/transport/ws.js +226 -0
- package/dist/vanilla/index.d.ts +657 -0
- package/dist/vanilla/index.js +278 -0
- package/package.json +253 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
9
|
+
var __toCommonJS = (from) => {
|
|
10
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
11
|
+
if (entry)
|
|
12
|
+
return entry;
|
|
13
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (var key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(entry, key))
|
|
17
|
+
__defProp(entry, key, {
|
|
18
|
+
get: __accessProp.bind(from, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
__moduleCache.set(from, entry);
|
|
23
|
+
return entry;
|
|
24
|
+
};
|
|
25
|
+
var __moduleCache;
|
|
26
|
+
var __returnValue = (v) => v;
|
|
27
|
+
function __exportSetter(name, newValue) {
|
|
28
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
29
|
+
}
|
|
30
|
+
var __export = (target, all) => {
|
|
31
|
+
for (var name in all)
|
|
32
|
+
__defProp(target, name, {
|
|
33
|
+
get: all[name],
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
set: __exportSetter.bind(all, name)
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
40
|
+
|
|
41
|
+
// src/server/drizzle.ts
|
|
42
|
+
var exports_drizzle = {};
|
|
43
|
+
__export(exports_drizzle, {
|
|
44
|
+
drizzleTxAtomic: () => drizzleTxAtomic,
|
|
45
|
+
drizzleTable: () => drizzleTable
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(exports_drizzle);
|
|
48
|
+
var import_node_module2 = require("node:module");
|
|
49
|
+
|
|
50
|
+
// src/server/factory.ts
|
|
51
|
+
function getByDottedPath(root, path) {
|
|
52
|
+
const parts = path.split(".");
|
|
53
|
+
let cur = root;
|
|
54
|
+
for (const p of parts) {
|
|
55
|
+
if (cur == null || typeof cur !== "object")
|
|
56
|
+
return;
|
|
57
|
+
cur = cur[p];
|
|
58
|
+
}
|
|
59
|
+
return cur;
|
|
60
|
+
}
|
|
61
|
+
function renderTemplate(template, ctx) {
|
|
62
|
+
return template.replace(/\$\{([^}]+)\}/g, (_m, expr) => {
|
|
63
|
+
const trimmed = expr.trim();
|
|
64
|
+
const path = trimmed.includes(".") ? trimmed : `params.${trimmed}`;
|
|
65
|
+
const v = getByDottedPath(ctx, path);
|
|
66
|
+
return v == null ? "" : String(v);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function resolveStamp(source, ctx) {
|
|
70
|
+
if (typeof source === "function")
|
|
71
|
+
return source(ctx);
|
|
72
|
+
if (source === "auth.userId")
|
|
73
|
+
return ctx.auth.userId;
|
|
74
|
+
if (source === "auth.name")
|
|
75
|
+
return ctx.auth.name;
|
|
76
|
+
if (typeof source === "string" && source.includes(".")) {
|
|
77
|
+
return getByDottedPath(ctx, source);
|
|
78
|
+
}
|
|
79
|
+
return source;
|
|
80
|
+
}
|
|
81
|
+
function buildScopeFilter(scope, ctx) {
|
|
82
|
+
if (!scope)
|
|
83
|
+
return;
|
|
84
|
+
if (typeof scope === "function")
|
|
85
|
+
return;
|
|
86
|
+
if (typeof scope === "string") {
|
|
87
|
+
const val2 = ctx.params[scope];
|
|
88
|
+
if (val2 === undefined) {
|
|
89
|
+
throw new Error(`params.${scope} required for scoped query`);
|
|
90
|
+
}
|
|
91
|
+
return { kind: "params", column: scope, value: val2 };
|
|
92
|
+
}
|
|
93
|
+
const { auth: authKey, column } = scope;
|
|
94
|
+
const val = ctx.auth[authKey];
|
|
95
|
+
if (val === undefined) {
|
|
96
|
+
throw new Error(`auth.${authKey} required for scoped query`);
|
|
97
|
+
}
|
|
98
|
+
return { kind: "auth", column, value: val };
|
|
99
|
+
}
|
|
100
|
+
function defineTable(adapter, opts = {}) {
|
|
101
|
+
const pkName = opts.pk ?? adapter.pk ?? "id";
|
|
102
|
+
const query = opts.query ? opts.query : (ctx, db) => {
|
|
103
|
+
if (typeof opts.scope === "function") {
|
|
104
|
+
throw new Error("function-form scope requires opts.query override on defineTable (no SQL fragment to pass to adapter)");
|
|
105
|
+
}
|
|
106
|
+
const scope = buildScopeFilter(opts.scope, ctx);
|
|
107
|
+
return adapter.list({ db, scope });
|
|
108
|
+
};
|
|
109
|
+
const authorize = async (action, ctx, _db) => {
|
|
110
|
+
if (action.type === "read" && opts.policy?.read) {
|
|
111
|
+
const ok = await opts.policy.read({ ctx });
|
|
112
|
+
if (!ok)
|
|
113
|
+
throw new Error("policy denied read");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const mutate = async (op, rawCtx, db) => {
|
|
117
|
+
const ctx = rawCtx;
|
|
118
|
+
const payload = op.payload ? { ...op.payload } : {};
|
|
119
|
+
if (op.type !== "delete")
|
|
120
|
+
delete payload[pkName];
|
|
121
|
+
if (opts.rowId) {
|
|
122
|
+
const expected = renderTemplate(opts.rowId, {
|
|
123
|
+
auth: ctx.auth,
|
|
124
|
+
params: ctx.params
|
|
125
|
+
});
|
|
126
|
+
if (op.rowId !== expected) {
|
|
127
|
+
throw new Error("rowId mismatch");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (opts.scope && typeof opts.scope === "string" && op.type === "insert") {
|
|
131
|
+
const col = opts.scope;
|
|
132
|
+
const paramVal = ctx.params[col];
|
|
133
|
+
if (paramVal === undefined) {
|
|
134
|
+
throw new Error(`params.${col} required for scoped mutation`);
|
|
135
|
+
}
|
|
136
|
+
if (payload[col] !== paramVal) {
|
|
137
|
+
throw new Error(`${col} mismatch`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (opts.scope && typeof opts.scope === "object" && !Array.isArray(opts.scope) && "auth" in opts.scope && op.type === "insert") {
|
|
141
|
+
const { auth: authKey, column } = opts.scope;
|
|
142
|
+
const authVal = ctx.auth[authKey];
|
|
143
|
+
if (authVal === undefined) {
|
|
144
|
+
throw new Error(`auth.${authKey} required for scoped mutation`);
|
|
145
|
+
}
|
|
146
|
+
if (payload[column] !== authVal) {
|
|
147
|
+
throw new Error(`${column} mismatch`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const existing = op.type !== "insert" ? await Promise.resolve(adapter.find({ db, rowId: op.rowId })) : null;
|
|
151
|
+
if (op.type !== "insert" && existing) {
|
|
152
|
+
if (opts.scope && typeof opts.scope === "string") {
|
|
153
|
+
const col = opts.scope;
|
|
154
|
+
const paramVal = ctx.params[col];
|
|
155
|
+
if (paramVal === undefined) {
|
|
156
|
+
throw new Error(`params.${col} required for scoped mutation`);
|
|
157
|
+
}
|
|
158
|
+
if (existing[col] !== paramVal) {
|
|
159
|
+
throw new Error(`${col} mismatch`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (opts.scope && typeof opts.scope === "object" && !Array.isArray(opts.scope) && "auth" in opts.scope) {
|
|
163
|
+
const { auth: authKey, column } = opts.scope;
|
|
164
|
+
const authVal = ctx.auth[authKey];
|
|
165
|
+
if (authVal === undefined) {
|
|
166
|
+
throw new Error(`auth.${authKey} required for scoped mutation`);
|
|
167
|
+
}
|
|
168
|
+
if (existing[column] !== authVal) {
|
|
169
|
+
throw new Error(`${column} mismatch`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (op.type === "insert" && opts.policy?.insert) {
|
|
174
|
+
const ok = await opts.policy.insert({ ctx, payload });
|
|
175
|
+
if (!ok)
|
|
176
|
+
throw new Error("policy denied insert");
|
|
177
|
+
} else if (op.type === "update" && opts.policy?.update) {
|
|
178
|
+
const ok = await opts.policy.update({
|
|
179
|
+
ctx,
|
|
180
|
+
existing: existing ?? {},
|
|
181
|
+
payload
|
|
182
|
+
});
|
|
183
|
+
if (!ok)
|
|
184
|
+
throw new Error("policy denied update");
|
|
185
|
+
} else if (op.type === "delete" && opts.policy?.delete) {
|
|
186
|
+
const ok = await opts.policy.delete({ ctx, existing: existing ?? {} });
|
|
187
|
+
if (!ok)
|
|
188
|
+
throw new Error("policy denied delete");
|
|
189
|
+
}
|
|
190
|
+
if (opts.policy?.fields && op.type !== "delete") {
|
|
191
|
+
for (const [field, rule] of Object.entries(opts.policy.fields)) {
|
|
192
|
+
if (rule === "server-only") {
|
|
193
|
+
delete payload[field];
|
|
194
|
+
} else if (rule === "insert-only") {
|
|
195
|
+
if (op.type === "update")
|
|
196
|
+
delete payload[field];
|
|
197
|
+
} else if (typeof rule === "object" && rule && "write" in rule) {
|
|
198
|
+
const allow = await rule.write({
|
|
199
|
+
ctx,
|
|
200
|
+
existing: existing ?? null,
|
|
201
|
+
payload
|
|
202
|
+
});
|
|
203
|
+
if (!allow)
|
|
204
|
+
delete payload[field];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (op.type === "insert" && opts.ownerStamp) {
|
|
209
|
+
for (const [field, source] of Object.entries(opts.ownerStamp)) {
|
|
210
|
+
if (source === undefined)
|
|
211
|
+
continue;
|
|
212
|
+
payload[field] = resolveStamp(source, ctx);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (op.type !== "delete" && opts.serverSet) {
|
|
216
|
+
for (const [field, val] of Object.entries(opts.serverSet)) {
|
|
217
|
+
if (val === undefined)
|
|
218
|
+
continue;
|
|
219
|
+
payload[field] = typeof val === "function" ? val(ctx) : val;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (op.type === "insert") {
|
|
223
|
+
await opts.hooks?.beforeInsert?.({ payload, ctx, db });
|
|
224
|
+
} else if (op.type === "update") {
|
|
225
|
+
await opts.hooks?.beforeUpdate?.({
|
|
226
|
+
payload,
|
|
227
|
+
existing: existing ?? {},
|
|
228
|
+
ctx,
|
|
229
|
+
db
|
|
230
|
+
});
|
|
231
|
+
} else if (op.type === "delete") {
|
|
232
|
+
await opts.hooks?.beforeDelete?.({ existing: existing ?? {}, ctx, db });
|
|
233
|
+
}
|
|
234
|
+
if (op.type === "delete") {
|
|
235
|
+
if (opts.delete) {
|
|
236
|
+
await opts.delete({ rowId: op.rowId, existing, ctx, db });
|
|
237
|
+
} else {
|
|
238
|
+
await Promise.resolve(adapter.delete({ db, rowId: op.rowId }));
|
|
239
|
+
}
|
|
240
|
+
} else if (op.type === "insert" && opts.insert) {
|
|
241
|
+
await opts.insert({ payload, existing, ctx, db });
|
|
242
|
+
} else if (op.type === "update" && opts.update) {
|
|
243
|
+
await opts.update({ payload, existing: existing ?? {}, ctx, db });
|
|
244
|
+
} else if (op.type === "update") {
|
|
245
|
+
if (Object.keys(payload).length > 0) {
|
|
246
|
+
await Promise.resolve(adapter.update({ db, rowId: op.rowId, payload }));
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
await Promise.resolve(adapter.insert({ db, rowId: op.rowId, payload }));
|
|
250
|
+
}
|
|
251
|
+
if (op.type === "delete") {
|
|
252
|
+
await opts.hooks?.afterDelete?.({ rowId: op.rowId, ctx, db });
|
|
253
|
+
} else {
|
|
254
|
+
const hookName = op.type === "insert" ? "afterInsert" : "afterUpdate";
|
|
255
|
+
const after = opts.hooks?.[hookName];
|
|
256
|
+
if (after) {
|
|
257
|
+
const row = await Promise.resolve(adapter.find({ db, rowId: op.rowId })) ?? {};
|
|
258
|
+
if (op.type === "insert") {
|
|
259
|
+
await opts.hooks?.afterInsert?.({ row, ctx, db });
|
|
260
|
+
} else {
|
|
261
|
+
await opts.hooks?.afterUpdate?.({
|
|
262
|
+
row,
|
|
263
|
+
previous: existing ?? {},
|
|
264
|
+
ctx,
|
|
265
|
+
db
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
return { query, mutate, authorize };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/server/drizzle.ts
|
|
275
|
+
var requireFn = import_node_module2.createRequire(module.filename);
|
|
276
|
+
var cachedDrizzle;
|
|
277
|
+
function loadDrizzleSync() {
|
|
278
|
+
if (cachedDrizzle === undefined) {
|
|
279
|
+
try {
|
|
280
|
+
const mod = requireFn("drizzle-orm");
|
|
281
|
+
cachedDrizzle = { eq: mod.eq, sql: mod.sql, getTableName: mod.getTableName };
|
|
282
|
+
} catch {
|
|
283
|
+
cachedDrizzle = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!cachedDrizzle) {
|
|
287
|
+
throw new Error("drizzleTable / drizzleTxAtomic require drizzle-orm to be installed. Add it to your dependencies, or use defineTable with a custom TableAdapter.");
|
|
288
|
+
}
|
|
289
|
+
return cachedDrizzle;
|
|
290
|
+
}
|
|
291
|
+
async function execAll(query) {
|
|
292
|
+
const q = query;
|
|
293
|
+
if (q && typeof q.all === "function") {
|
|
294
|
+
return await Promise.resolve(q.all());
|
|
295
|
+
}
|
|
296
|
+
const res = await Promise.resolve(query);
|
|
297
|
+
return Array.isArray(res) ? res : [];
|
|
298
|
+
}
|
|
299
|
+
function drizzleAdapter(table, pk) {
|
|
300
|
+
const pkName = pk ?? "id";
|
|
301
|
+
const pkCol = table[pkName];
|
|
302
|
+
let cachedName = "";
|
|
303
|
+
const adapter = {
|
|
304
|
+
get name() {
|
|
305
|
+
return cachedName;
|
|
306
|
+
},
|
|
307
|
+
pk: pkName,
|
|
308
|
+
async list({ db, scope }) {
|
|
309
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
310
|
+
if (!cachedName)
|
|
311
|
+
cachedName = getTableName(table);
|
|
312
|
+
if (!scope) {
|
|
313
|
+
return await execAll(db.select().from(table));
|
|
314
|
+
}
|
|
315
|
+
const col = table[scope.column];
|
|
316
|
+
return await execAll(db.select().from(table).where(eq(col, scope.value)));
|
|
317
|
+
},
|
|
318
|
+
async find({ db, rowId }) {
|
|
319
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
320
|
+
if (!cachedName)
|
|
321
|
+
cachedName = getTableName(table);
|
|
322
|
+
const rows = await execAll(db.select().from(table).where(eq(pkCol, rowId)));
|
|
323
|
+
return rows[0] ?? null;
|
|
324
|
+
},
|
|
325
|
+
async insert({ db, rowId, payload }) {
|
|
326
|
+
const { getTableName } = loadDrizzleSync();
|
|
327
|
+
if (!cachedName)
|
|
328
|
+
cachedName = getTableName(table);
|
|
329
|
+
const values = { ...payload, [pkName]: rowId };
|
|
330
|
+
if (Object.keys(payload).length > 0) {
|
|
331
|
+
await Promise.resolve(db.insert(table).values(values).onConflictDoUpdate({ target: pkCol, set: payload }));
|
|
332
|
+
} else {
|
|
333
|
+
await Promise.resolve(db.insert(table).values(values).onConflictDoNothing());
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
async update({ db, rowId, payload }) {
|
|
337
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
338
|
+
if (!cachedName)
|
|
339
|
+
cachedName = getTableName(table);
|
|
340
|
+
await Promise.resolve(db.update(table).set(payload).where(eq(pkCol, rowId)));
|
|
341
|
+
},
|
|
342
|
+
async delete({ db, rowId }) {
|
|
343
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
344
|
+
if (!cachedName)
|
|
345
|
+
cachedName = getTableName(table);
|
|
346
|
+
await Promise.resolve(db.delete(table).where(eq(pkCol, rowId)));
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
return adapter;
|
|
350
|
+
}
|
|
351
|
+
function drizzleQueryFn(table, opts) {
|
|
352
|
+
if (opts.query)
|
|
353
|
+
return opts.query;
|
|
354
|
+
return (ctx, db) => {
|
|
355
|
+
const where = buildDrizzleScopeWhere(opts.scope, ctx, table);
|
|
356
|
+
return where ? db.select().from(table).where(where) : db.select().from(table);
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function buildDrizzleScopeWhere(scope, ctx, table) {
|
|
360
|
+
if (!scope)
|
|
361
|
+
return;
|
|
362
|
+
if (typeof scope === "function")
|
|
363
|
+
return scope(ctx);
|
|
364
|
+
const { eq } = loadDrizzleSync();
|
|
365
|
+
if (typeof scope === "string") {
|
|
366
|
+
const col2 = table[scope];
|
|
367
|
+
const val2 = ctx.params[scope];
|
|
368
|
+
if (val2 === undefined) {
|
|
369
|
+
throw new Error(`params.${String(scope)} required for scoped query`);
|
|
370
|
+
}
|
|
371
|
+
return eq(col2, val2);
|
|
372
|
+
}
|
|
373
|
+
const col = table[scope.column];
|
|
374
|
+
const val = ctx.auth[scope.auth];
|
|
375
|
+
if (val === undefined) {
|
|
376
|
+
throw new Error(`auth.${String(scope.auth)} required for scoped query`);
|
|
377
|
+
}
|
|
378
|
+
return eq(col, val);
|
|
379
|
+
}
|
|
380
|
+
function drizzleTable(table, opts = {}) {
|
|
381
|
+
const adapter = drizzleAdapter(table, opts.pk);
|
|
382
|
+
const query = drizzleQueryFn(table, opts);
|
|
383
|
+
return defineTable(adapter, {
|
|
384
|
+
...opts,
|
|
385
|
+
query
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
var drizzleTxAtomic = {
|
|
389
|
+
async begin(db) {
|
|
390
|
+
const { sql } = loadDrizzleSync();
|
|
391
|
+
const handle = db;
|
|
392
|
+
if (typeof handle?.run !== "function") {
|
|
393
|
+
throw new Error("drizzleTxAtomic.begin requires a drizzle db with a run() method");
|
|
394
|
+
}
|
|
395
|
+
await Promise.resolve(handle.run(sql`BEGIN`));
|
|
396
|
+
},
|
|
397
|
+
async commit(db) {
|
|
398
|
+
const { sql } = loadDrizzleSync();
|
|
399
|
+
const handle = db;
|
|
400
|
+
if (typeof handle?.run !== "function") {
|
|
401
|
+
throw new Error("drizzleTxAtomic.commit requires a drizzle db with a run() method");
|
|
402
|
+
}
|
|
403
|
+
await Promise.resolve(handle.run(sql`COMMIT`));
|
|
404
|
+
},
|
|
405
|
+
async rollback(db) {
|
|
406
|
+
const { sql } = loadDrizzleSync();
|
|
407
|
+
const handle = db;
|
|
408
|
+
if (typeof handle?.run !== "function") {
|
|
409
|
+
throw new Error("drizzleTxAtomic.rollback requires a drizzle db with a run() method");
|
|
410
|
+
}
|
|
411
|
+
await Promise.resolve(handle.run(sql`ROLLBACK`));
|
|
412
|
+
}
|
|
413
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
type OpType = "insert" | "update" | "delete";
|
|
2
|
+
interface AuthContext {
|
|
3
|
+
userId: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface DrizzleTableLike {
|
|
7
|
+
$inferSelect: Record<string, unknown>;
|
|
8
|
+
$inferInsert: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Adapter contract for `server.tx({ atomic: true })` — pluggable so non-drizzle
|
|
12
|
+
* data layers (kysely, prisma, raw SQL) can wrap their own BEGIN/COMMIT/ROLLBACK
|
|
13
|
+
* without reflectdb hard-coding a drizzle dependency.
|
|
14
|
+
*
|
|
15
|
+
* Pass an adapter via:
|
|
16
|
+
* - `server.tx({ atomic: <adapter> }, fn)` — per-call override.
|
|
17
|
+
* - `ServerConfig.txAtomic` — server-wide default for `atomic: true`.
|
|
18
|
+
*
|
|
19
|
+
* If neither is supplied and `atomic: true` is requested, reflectdb falls back to
|
|
20
|
+
* the bundled drizzle adapter via dynamic import (no top-level dep). When
|
|
21
|
+
* drizzle isn't installed the fallback throws a clear error directing users to
|
|
22
|
+
* supply a `txAtomic` adapter.
|
|
23
|
+
*/
|
|
24
|
+
interface TxAtomicAdapter {
|
|
25
|
+
/** Begin a transaction on the given db handle. */
|
|
26
|
+
begin(db: unknown): Promise<void>;
|
|
27
|
+
/** Commit the in-flight transaction. */
|
|
28
|
+
commit(db: unknown): Promise<void>;
|
|
29
|
+
/** Roll back the in-flight transaction. */
|
|
30
|
+
rollback(db: unknown): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
interface ResolvedOp {
|
|
33
|
+
type: OpType;
|
|
34
|
+
table: string;
|
|
35
|
+
rowId: string;
|
|
36
|
+
payload: Record<string, unknown> | null;
|
|
37
|
+
hlc: string;
|
|
38
|
+
}
|
|
39
|
+
interface MutationContext<TAuth extends AuthContext = AuthContext> {
|
|
40
|
+
auth: TAuth;
|
|
41
|
+
params: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
type AuthorizeAction = {
|
|
44
|
+
type: "read";
|
|
45
|
+
table: string;
|
|
46
|
+
params: Record<string, unknown>;
|
|
47
|
+
} | {
|
|
48
|
+
type: "write";
|
|
49
|
+
table: string;
|
|
50
|
+
op: ResolvedOp;
|
|
51
|
+
};
|
|
52
|
+
type Ctx<
|
|
53
|
+
TAuth extends AuthContext,
|
|
54
|
+
TParams extends Record<string, unknown>
|
|
55
|
+
> = {
|
|
56
|
+
auth: TAuth;
|
|
57
|
+
params: TParams;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Function-form scope returns an adapter-interpreted SQL fragment (e.g. a
|
|
61
|
+
* drizzle `SQL`, a kysely `Expression<boolean>`, or a raw string). The generic
|
|
62
|
+
* factory treats it as opaque (`unknown`) — ORM-specific helpers like
|
|
63
|
+
* `drizzleTable` narrow the return type at their call site.
|
|
64
|
+
*/
|
|
65
|
+
type ScopeFn<
|
|
66
|
+
TAuth extends AuthContext,
|
|
67
|
+
TParams extends Record<string, unknown>
|
|
68
|
+
> = (ctx: Ctx<TAuth, TParams>) => unknown;
|
|
69
|
+
type ScopeOpt<
|
|
70
|
+
TRowKey extends string,
|
|
71
|
+
TAuth extends AuthContext,
|
|
72
|
+
TParams extends Record<string, unknown>
|
|
73
|
+
> = TRowKey | {
|
|
74
|
+
auth: keyof TAuth & string;
|
|
75
|
+
column: TRowKey;
|
|
76
|
+
} | ScopeFn<TAuth, TParams>;
|
|
77
|
+
type StampSource<
|
|
78
|
+
TAuth extends AuthContext,
|
|
79
|
+
TParams extends Record<string, unknown>
|
|
80
|
+
> = "auth.userId" | "auth.name" | string | ((ctx: Ctx<TAuth, TParams>) => unknown);
|
|
81
|
+
type ServerSetSource<
|
|
82
|
+
TAuth extends AuthContext,
|
|
83
|
+
TParams extends Record<string, unknown>
|
|
84
|
+
> = unknown | ((ctx: Ctx<TAuth, TParams>) => unknown);
|
|
85
|
+
type FieldPolicy<
|
|
86
|
+
TAuth extends AuthContext,
|
|
87
|
+
TParams extends Record<string, unknown>
|
|
88
|
+
> = "server-only" | "insert-only" | {
|
|
89
|
+
write: (input: {
|
|
90
|
+
ctx: Ctx<TAuth, TParams>;
|
|
91
|
+
existing?: Record<string, unknown> | null;
|
|
92
|
+
payload: Record<string, unknown>;
|
|
93
|
+
}) => boolean | Promise<boolean>;
|
|
94
|
+
};
|
|
95
|
+
interface DefineTableOpts<
|
|
96
|
+
TRow extends Record<string, unknown> = Record<string, unknown>,
|
|
97
|
+
TAuth extends AuthContext = AuthContext,
|
|
98
|
+
TParams extends Record<string, unknown> = Record<string, unknown>
|
|
99
|
+
> {
|
|
100
|
+
scope?: ScopeOpt<keyof TRow & string, TAuth, TParams>;
|
|
101
|
+
rowId?: string;
|
|
102
|
+
ownerStamp?: Partial<Record<keyof TRow & string, StampSource<TAuth, TParams>>>;
|
|
103
|
+
serverSet?: Partial<Record<keyof TRow & string, ServerSetSource<TAuth, TParams>>>;
|
|
104
|
+
policy?: {
|
|
105
|
+
read?: (input: {
|
|
106
|
+
ctx: Ctx<TAuth, TParams>;
|
|
107
|
+
existing?: Record<string, unknown> | null;
|
|
108
|
+
}) => boolean | Promise<boolean>;
|
|
109
|
+
insert?: (input: {
|
|
110
|
+
ctx: Ctx<TAuth, TParams>;
|
|
111
|
+
payload: Record<string, unknown>;
|
|
112
|
+
}) => boolean | Promise<boolean>;
|
|
113
|
+
update?: (input: {
|
|
114
|
+
ctx: Ctx<TAuth, TParams>;
|
|
115
|
+
existing: Record<string, unknown>;
|
|
116
|
+
payload: Record<string, unknown>;
|
|
117
|
+
}) => boolean | Promise<boolean>;
|
|
118
|
+
delete?: (input: {
|
|
119
|
+
ctx: Ctx<TAuth, TParams>;
|
|
120
|
+
existing: Record<string, unknown>;
|
|
121
|
+
}) => boolean | Promise<boolean>;
|
|
122
|
+
fields?: Partial<Record<keyof TRow & string, FieldPolicy<TAuth, TParams>>>;
|
|
123
|
+
};
|
|
124
|
+
hooks?: {
|
|
125
|
+
beforeInsert?: (input: {
|
|
126
|
+
payload: Record<string, unknown>;
|
|
127
|
+
ctx: Ctx<TAuth, TParams>;
|
|
128
|
+
db: any;
|
|
129
|
+
}) => Promise<void> | void;
|
|
130
|
+
afterInsert?: (input: {
|
|
131
|
+
row: Record<string, unknown>;
|
|
132
|
+
ctx: Ctx<TAuth, TParams>;
|
|
133
|
+
db: any;
|
|
134
|
+
}) => Promise<void> | void;
|
|
135
|
+
beforeUpdate?: (input: {
|
|
136
|
+
payload: Record<string, unknown>;
|
|
137
|
+
existing: Record<string, unknown>;
|
|
138
|
+
ctx: Ctx<TAuth, TParams>;
|
|
139
|
+
db: any;
|
|
140
|
+
}) => Promise<void> | void;
|
|
141
|
+
afterUpdate?: (input: {
|
|
142
|
+
row: Record<string, unknown>;
|
|
143
|
+
previous: Record<string, unknown>;
|
|
144
|
+
ctx: Ctx<TAuth, TParams>;
|
|
145
|
+
db: any;
|
|
146
|
+
}) => Promise<void> | void;
|
|
147
|
+
beforeDelete?: (input: {
|
|
148
|
+
existing: Record<string, unknown>;
|
|
149
|
+
ctx: Ctx<TAuth, TParams>;
|
|
150
|
+
db: any;
|
|
151
|
+
}) => Promise<void> | void;
|
|
152
|
+
afterDelete?: (input: {
|
|
153
|
+
rowId: string;
|
|
154
|
+
ctx: Ctx<TAuth, TParams>;
|
|
155
|
+
db: any;
|
|
156
|
+
}) => Promise<void> | void;
|
|
157
|
+
};
|
|
158
|
+
query?: (ctx: Ctx<TAuth, TParams>, db: any) => unknown;
|
|
159
|
+
insert?: (input: {
|
|
160
|
+
payload: Record<string, unknown>;
|
|
161
|
+
existing?: Record<string, unknown> | null;
|
|
162
|
+
ctx: Ctx<TAuth, TParams>;
|
|
163
|
+
db: any;
|
|
164
|
+
}) => Promise<void> | void;
|
|
165
|
+
update?: (input: {
|
|
166
|
+
payload: Record<string, unknown>;
|
|
167
|
+
existing: Record<string, unknown>;
|
|
168
|
+
ctx: Ctx<TAuth, TParams>;
|
|
169
|
+
db: any;
|
|
170
|
+
}) => Promise<void> | void;
|
|
171
|
+
delete?: (input: {
|
|
172
|
+
rowId: string;
|
|
173
|
+
existing: Record<string, unknown> | null;
|
|
174
|
+
ctx: Ctx<TAuth, TParams>;
|
|
175
|
+
db: any;
|
|
176
|
+
}) => Promise<void> | void;
|
|
177
|
+
pk?: keyof TRow & string;
|
|
178
|
+
}
|
|
179
|
+
import { SQL as SQL_1rji } from "drizzle-orm";
|
|
180
|
+
type SQL = SQL_1rji;
|
|
181
|
+
type Ctx2<
|
|
182
|
+
TAuth extends AuthContext,
|
|
183
|
+
TParams extends Record<string, unknown>
|
|
184
|
+
> = {
|
|
185
|
+
auth: TAuth;
|
|
186
|
+
params: TParams;
|
|
187
|
+
};
|
|
188
|
+
type DrizzleScopeFn<
|
|
189
|
+
TAuth extends AuthContext,
|
|
190
|
+
TParams extends Record<string, unknown>
|
|
191
|
+
> = (ctx: Ctx2<TAuth, TParams>) => SQL;
|
|
192
|
+
type DrizzleScopeOpt<
|
|
193
|
+
TRowKey extends string,
|
|
194
|
+
TAuth extends AuthContext,
|
|
195
|
+
TParams extends Record<string, unknown>
|
|
196
|
+
> = TRowKey | {
|
|
197
|
+
auth: keyof TAuth & string;
|
|
198
|
+
column: TRowKey;
|
|
199
|
+
} | DrizzleScopeFn<TAuth, TParams>;
|
|
200
|
+
/**
|
|
201
|
+
* Drizzle-flavored options. Identical surface to `DefineTableOpts` except the
|
|
202
|
+
* row type is derived from `$inferSelect` and function-form scope returns
|
|
203
|
+
* drizzle's `SQL` (vs. the generic factory's `unknown`). Kept for backward
|
|
204
|
+
* compat with the existing public `drizzleTable<TTable, ...>(opts)` call shape.
|
|
205
|
+
*/
|
|
206
|
+
interface DrizzleTableOpts<
|
|
207
|
+
TTable extends DrizzleTableLike,
|
|
208
|
+
TAuth extends AuthContext = AuthContext,
|
|
209
|
+
TParams extends Record<string, unknown> = Record<string, unknown>
|
|
210
|
+
> extends Omit<DefineTableOpts<TTable["$inferSelect"], TAuth, TParams>, "scope"> {
|
|
211
|
+
scope?: DrizzleScopeOpt<keyof TTable["$inferSelect"] & string, TAuth, TParams>;
|
|
212
|
+
}
|
|
213
|
+
declare function drizzleTable<
|
|
214
|
+
TTable extends DrizzleTableLike,
|
|
215
|
+
TAuth extends AuthContext = AuthContext,
|
|
216
|
+
TParams extends Record<string, unknown> = Record<string, unknown>
|
|
217
|
+
>(table: TTable, opts?: DrizzleTableOpts<TTable, TAuth, TParams>): {
|
|
218
|
+
query: (ctx: Ctx2<TAuth, TParams>, db: any) => unknown;
|
|
219
|
+
mutate: (op: ResolvedOp, ctx: MutationContext<TAuth>, db: any) => Promise<void>;
|
|
220
|
+
authorize: (action: AuthorizeAction, ctx: MutationContext<TAuth>, db: any) => Promise<void>;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* `TxAtomicAdapter` for drizzle handles. Uses raw SQL via `db.run(sql\`BEGIN\`)`
|
|
224
|
+
* — drizzle's async `db.transaction(...)` is unsafe on the bun-sqlite dialect
|
|
225
|
+
* (sync transaction commits before the async body finishes), so we route
|
|
226
|
+
* through `db.run(...)` which is portable across sqlite and postgres adapters.
|
|
227
|
+
*
|
|
228
|
+
* Exported so users can pass it explicitly to `tx({ atomic: drizzleTxAtomic })`
|
|
229
|
+
* or to `ServerConfig.txAtomic` — and so the server's lazy-loaded fallback
|
|
230
|
+
* has a single source of truth.
|
|
231
|
+
*/
|
|
232
|
+
declare const drizzleTxAtomic: TxAtomicAdapter;
|
|
233
|
+
export { drizzleTxAtomic, drizzleTable, DrizzleTableOpts };
|