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,369 @@
|
|
|
1
|
+
// src/server/drizzle.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
// src/server/factory.ts
|
|
5
|
+
function getByDottedPath(root, path) {
|
|
6
|
+
const parts = path.split(".");
|
|
7
|
+
let cur = root;
|
|
8
|
+
for (const p of parts) {
|
|
9
|
+
if (cur == null || typeof cur !== "object")
|
|
10
|
+
return;
|
|
11
|
+
cur = cur[p];
|
|
12
|
+
}
|
|
13
|
+
return cur;
|
|
14
|
+
}
|
|
15
|
+
function renderTemplate(template, ctx) {
|
|
16
|
+
return template.replace(/\$\{([^}]+)\}/g, (_m, expr) => {
|
|
17
|
+
const trimmed = expr.trim();
|
|
18
|
+
const path = trimmed.includes(".") ? trimmed : `params.${trimmed}`;
|
|
19
|
+
const v = getByDottedPath(ctx, path);
|
|
20
|
+
return v == null ? "" : String(v);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function resolveStamp(source, ctx) {
|
|
24
|
+
if (typeof source === "function")
|
|
25
|
+
return source(ctx);
|
|
26
|
+
if (source === "auth.userId")
|
|
27
|
+
return ctx.auth.userId;
|
|
28
|
+
if (source === "auth.name")
|
|
29
|
+
return ctx.auth.name;
|
|
30
|
+
if (typeof source === "string" && source.includes(".")) {
|
|
31
|
+
return getByDottedPath(ctx, source);
|
|
32
|
+
}
|
|
33
|
+
return source;
|
|
34
|
+
}
|
|
35
|
+
function buildScopeFilter(scope, ctx) {
|
|
36
|
+
if (!scope)
|
|
37
|
+
return;
|
|
38
|
+
if (typeof scope === "function")
|
|
39
|
+
return;
|
|
40
|
+
if (typeof scope === "string") {
|
|
41
|
+
const val2 = ctx.params[scope];
|
|
42
|
+
if (val2 === undefined) {
|
|
43
|
+
throw new Error(`params.${scope} required for scoped query`);
|
|
44
|
+
}
|
|
45
|
+
return { kind: "params", column: scope, value: val2 };
|
|
46
|
+
}
|
|
47
|
+
const { auth: authKey, column } = scope;
|
|
48
|
+
const val = ctx.auth[authKey];
|
|
49
|
+
if (val === undefined) {
|
|
50
|
+
throw new Error(`auth.${authKey} required for scoped query`);
|
|
51
|
+
}
|
|
52
|
+
return { kind: "auth", column, value: val };
|
|
53
|
+
}
|
|
54
|
+
function defineTable(adapter, opts = {}) {
|
|
55
|
+
const pkName = opts.pk ?? adapter.pk ?? "id";
|
|
56
|
+
const query = opts.query ? opts.query : (ctx, db) => {
|
|
57
|
+
if (typeof opts.scope === "function") {
|
|
58
|
+
throw new Error("function-form scope requires opts.query override on defineTable (no SQL fragment to pass to adapter)");
|
|
59
|
+
}
|
|
60
|
+
const scope = buildScopeFilter(opts.scope, ctx);
|
|
61
|
+
return adapter.list({ db, scope });
|
|
62
|
+
};
|
|
63
|
+
const authorize = async (action, ctx, _db) => {
|
|
64
|
+
if (action.type === "read" && opts.policy?.read) {
|
|
65
|
+
const ok = await opts.policy.read({ ctx });
|
|
66
|
+
if (!ok)
|
|
67
|
+
throw new Error("policy denied read");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const mutate = async (op, rawCtx, db) => {
|
|
71
|
+
const ctx = rawCtx;
|
|
72
|
+
const payload = op.payload ? { ...op.payload } : {};
|
|
73
|
+
if (op.type !== "delete")
|
|
74
|
+
delete payload[pkName];
|
|
75
|
+
if (opts.rowId) {
|
|
76
|
+
const expected = renderTemplate(opts.rowId, {
|
|
77
|
+
auth: ctx.auth,
|
|
78
|
+
params: ctx.params
|
|
79
|
+
});
|
|
80
|
+
if (op.rowId !== expected) {
|
|
81
|
+
throw new Error("rowId mismatch");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (opts.scope && typeof opts.scope === "string" && op.type === "insert") {
|
|
85
|
+
const col = opts.scope;
|
|
86
|
+
const paramVal = ctx.params[col];
|
|
87
|
+
if (paramVal === undefined) {
|
|
88
|
+
throw new Error(`params.${col} required for scoped mutation`);
|
|
89
|
+
}
|
|
90
|
+
if (payload[col] !== paramVal) {
|
|
91
|
+
throw new Error(`${col} mismatch`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (opts.scope && typeof opts.scope === "object" && !Array.isArray(opts.scope) && "auth" in opts.scope && op.type === "insert") {
|
|
95
|
+
const { auth: authKey, column } = opts.scope;
|
|
96
|
+
const authVal = ctx.auth[authKey];
|
|
97
|
+
if (authVal === undefined) {
|
|
98
|
+
throw new Error(`auth.${authKey} required for scoped mutation`);
|
|
99
|
+
}
|
|
100
|
+
if (payload[column] !== authVal) {
|
|
101
|
+
throw new Error(`${column} mismatch`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const existing = op.type !== "insert" ? await Promise.resolve(adapter.find({ db, rowId: op.rowId })) : null;
|
|
105
|
+
if (op.type !== "insert" && existing) {
|
|
106
|
+
if (opts.scope && typeof opts.scope === "string") {
|
|
107
|
+
const col = opts.scope;
|
|
108
|
+
const paramVal = ctx.params[col];
|
|
109
|
+
if (paramVal === undefined) {
|
|
110
|
+
throw new Error(`params.${col} required for scoped mutation`);
|
|
111
|
+
}
|
|
112
|
+
if (existing[col] !== paramVal) {
|
|
113
|
+
throw new Error(`${col} mismatch`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (opts.scope && typeof opts.scope === "object" && !Array.isArray(opts.scope) && "auth" in opts.scope) {
|
|
117
|
+
const { auth: authKey, column } = opts.scope;
|
|
118
|
+
const authVal = ctx.auth[authKey];
|
|
119
|
+
if (authVal === undefined) {
|
|
120
|
+
throw new Error(`auth.${authKey} required for scoped mutation`);
|
|
121
|
+
}
|
|
122
|
+
if (existing[column] !== authVal) {
|
|
123
|
+
throw new Error(`${column} mismatch`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (op.type === "insert" && opts.policy?.insert) {
|
|
128
|
+
const ok = await opts.policy.insert({ ctx, payload });
|
|
129
|
+
if (!ok)
|
|
130
|
+
throw new Error("policy denied insert");
|
|
131
|
+
} else if (op.type === "update" && opts.policy?.update) {
|
|
132
|
+
const ok = await opts.policy.update({
|
|
133
|
+
ctx,
|
|
134
|
+
existing: existing ?? {},
|
|
135
|
+
payload
|
|
136
|
+
});
|
|
137
|
+
if (!ok)
|
|
138
|
+
throw new Error("policy denied update");
|
|
139
|
+
} else if (op.type === "delete" && opts.policy?.delete) {
|
|
140
|
+
const ok = await opts.policy.delete({ ctx, existing: existing ?? {} });
|
|
141
|
+
if (!ok)
|
|
142
|
+
throw new Error("policy denied delete");
|
|
143
|
+
}
|
|
144
|
+
if (opts.policy?.fields && op.type !== "delete") {
|
|
145
|
+
for (const [field, rule] of Object.entries(opts.policy.fields)) {
|
|
146
|
+
if (rule === "server-only") {
|
|
147
|
+
delete payload[field];
|
|
148
|
+
} else if (rule === "insert-only") {
|
|
149
|
+
if (op.type === "update")
|
|
150
|
+
delete payload[field];
|
|
151
|
+
} else if (typeof rule === "object" && rule && "write" in rule) {
|
|
152
|
+
const allow = await rule.write({
|
|
153
|
+
ctx,
|
|
154
|
+
existing: existing ?? null,
|
|
155
|
+
payload
|
|
156
|
+
});
|
|
157
|
+
if (!allow)
|
|
158
|
+
delete payload[field];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (op.type === "insert" && opts.ownerStamp) {
|
|
163
|
+
for (const [field, source] of Object.entries(opts.ownerStamp)) {
|
|
164
|
+
if (source === undefined)
|
|
165
|
+
continue;
|
|
166
|
+
payload[field] = resolveStamp(source, ctx);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (op.type !== "delete" && opts.serverSet) {
|
|
170
|
+
for (const [field, val] of Object.entries(opts.serverSet)) {
|
|
171
|
+
if (val === undefined)
|
|
172
|
+
continue;
|
|
173
|
+
payload[field] = typeof val === "function" ? val(ctx) : val;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (op.type === "insert") {
|
|
177
|
+
await opts.hooks?.beforeInsert?.({ payload, ctx, db });
|
|
178
|
+
} else if (op.type === "update") {
|
|
179
|
+
await opts.hooks?.beforeUpdate?.({
|
|
180
|
+
payload,
|
|
181
|
+
existing: existing ?? {},
|
|
182
|
+
ctx,
|
|
183
|
+
db
|
|
184
|
+
});
|
|
185
|
+
} else if (op.type === "delete") {
|
|
186
|
+
await opts.hooks?.beforeDelete?.({ existing: existing ?? {}, ctx, db });
|
|
187
|
+
}
|
|
188
|
+
if (op.type === "delete") {
|
|
189
|
+
if (opts.delete) {
|
|
190
|
+
await opts.delete({ rowId: op.rowId, existing, ctx, db });
|
|
191
|
+
} else {
|
|
192
|
+
await Promise.resolve(adapter.delete({ db, rowId: op.rowId }));
|
|
193
|
+
}
|
|
194
|
+
} else if (op.type === "insert" && opts.insert) {
|
|
195
|
+
await opts.insert({ payload, existing, ctx, db });
|
|
196
|
+
} else if (op.type === "update" && opts.update) {
|
|
197
|
+
await opts.update({ payload, existing: existing ?? {}, ctx, db });
|
|
198
|
+
} else if (op.type === "update") {
|
|
199
|
+
if (Object.keys(payload).length > 0) {
|
|
200
|
+
await Promise.resolve(adapter.update({ db, rowId: op.rowId, payload }));
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
await Promise.resolve(adapter.insert({ db, rowId: op.rowId, payload }));
|
|
204
|
+
}
|
|
205
|
+
if (op.type === "delete") {
|
|
206
|
+
await opts.hooks?.afterDelete?.({ rowId: op.rowId, ctx, db });
|
|
207
|
+
} else {
|
|
208
|
+
const hookName = op.type === "insert" ? "afterInsert" : "afterUpdate";
|
|
209
|
+
const after = opts.hooks?.[hookName];
|
|
210
|
+
if (after) {
|
|
211
|
+
const row = await Promise.resolve(adapter.find({ db, rowId: op.rowId })) ?? {};
|
|
212
|
+
if (op.type === "insert") {
|
|
213
|
+
await opts.hooks?.afterInsert?.({ row, ctx, db });
|
|
214
|
+
} else {
|
|
215
|
+
await opts.hooks?.afterUpdate?.({
|
|
216
|
+
row,
|
|
217
|
+
previous: existing ?? {},
|
|
218
|
+
ctx,
|
|
219
|
+
db
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
return { query, mutate, authorize };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/server/drizzle.ts
|
|
229
|
+
var requireFn = createRequire(import.meta.url);
|
|
230
|
+
var cachedDrizzle;
|
|
231
|
+
function loadDrizzleSync() {
|
|
232
|
+
if (cachedDrizzle === undefined) {
|
|
233
|
+
try {
|
|
234
|
+
const mod = requireFn("drizzle-orm");
|
|
235
|
+
cachedDrizzle = { eq: mod.eq, sql: mod.sql, getTableName: mod.getTableName };
|
|
236
|
+
} catch {
|
|
237
|
+
cachedDrizzle = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!cachedDrizzle) {
|
|
241
|
+
throw new Error("drizzleTable / drizzleTxAtomic require drizzle-orm to be installed. Add it to your dependencies, or use defineTable with a custom TableAdapter.");
|
|
242
|
+
}
|
|
243
|
+
return cachedDrizzle;
|
|
244
|
+
}
|
|
245
|
+
async function execAll(query) {
|
|
246
|
+
const q = query;
|
|
247
|
+
if (q && typeof q.all === "function") {
|
|
248
|
+
return await Promise.resolve(q.all());
|
|
249
|
+
}
|
|
250
|
+
const res = await Promise.resolve(query);
|
|
251
|
+
return Array.isArray(res) ? res : [];
|
|
252
|
+
}
|
|
253
|
+
function drizzleAdapter(table, pk) {
|
|
254
|
+
const pkName = pk ?? "id";
|
|
255
|
+
const pkCol = table[pkName];
|
|
256
|
+
let cachedName = "";
|
|
257
|
+
const adapter = {
|
|
258
|
+
get name() {
|
|
259
|
+
return cachedName;
|
|
260
|
+
},
|
|
261
|
+
pk: pkName,
|
|
262
|
+
async list({ db, scope }) {
|
|
263
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
264
|
+
if (!cachedName)
|
|
265
|
+
cachedName = getTableName(table);
|
|
266
|
+
if (!scope) {
|
|
267
|
+
return await execAll(db.select().from(table));
|
|
268
|
+
}
|
|
269
|
+
const col = table[scope.column];
|
|
270
|
+
return await execAll(db.select().from(table).where(eq(col, scope.value)));
|
|
271
|
+
},
|
|
272
|
+
async find({ db, rowId }) {
|
|
273
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
274
|
+
if (!cachedName)
|
|
275
|
+
cachedName = getTableName(table);
|
|
276
|
+
const rows = await execAll(db.select().from(table).where(eq(pkCol, rowId)));
|
|
277
|
+
return rows[0] ?? null;
|
|
278
|
+
},
|
|
279
|
+
async insert({ db, rowId, payload }) {
|
|
280
|
+
const { getTableName } = loadDrizzleSync();
|
|
281
|
+
if (!cachedName)
|
|
282
|
+
cachedName = getTableName(table);
|
|
283
|
+
const values = { ...payload, [pkName]: rowId };
|
|
284
|
+
if (Object.keys(payload).length > 0) {
|
|
285
|
+
await Promise.resolve(db.insert(table).values(values).onConflictDoUpdate({ target: pkCol, set: payload }));
|
|
286
|
+
} else {
|
|
287
|
+
await Promise.resolve(db.insert(table).values(values).onConflictDoNothing());
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
async update({ db, rowId, payload }) {
|
|
291
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
292
|
+
if (!cachedName)
|
|
293
|
+
cachedName = getTableName(table);
|
|
294
|
+
await Promise.resolve(db.update(table).set(payload).where(eq(pkCol, rowId)));
|
|
295
|
+
},
|
|
296
|
+
async delete({ db, rowId }) {
|
|
297
|
+
const { eq, getTableName } = loadDrizzleSync();
|
|
298
|
+
if (!cachedName)
|
|
299
|
+
cachedName = getTableName(table);
|
|
300
|
+
await Promise.resolve(db.delete(table).where(eq(pkCol, rowId)));
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
return adapter;
|
|
304
|
+
}
|
|
305
|
+
function drizzleQueryFn(table, opts) {
|
|
306
|
+
if (opts.query)
|
|
307
|
+
return opts.query;
|
|
308
|
+
return (ctx, db) => {
|
|
309
|
+
const where = buildDrizzleScopeWhere(opts.scope, ctx, table);
|
|
310
|
+
return where ? db.select().from(table).where(where) : db.select().from(table);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function buildDrizzleScopeWhere(scope, ctx, table) {
|
|
314
|
+
if (!scope)
|
|
315
|
+
return;
|
|
316
|
+
if (typeof scope === "function")
|
|
317
|
+
return scope(ctx);
|
|
318
|
+
const { eq } = loadDrizzleSync();
|
|
319
|
+
if (typeof scope === "string") {
|
|
320
|
+
const col2 = table[scope];
|
|
321
|
+
const val2 = ctx.params[scope];
|
|
322
|
+
if (val2 === undefined) {
|
|
323
|
+
throw new Error(`params.${String(scope)} required for scoped query`);
|
|
324
|
+
}
|
|
325
|
+
return eq(col2, val2);
|
|
326
|
+
}
|
|
327
|
+
const col = table[scope.column];
|
|
328
|
+
const val = ctx.auth[scope.auth];
|
|
329
|
+
if (val === undefined) {
|
|
330
|
+
throw new Error(`auth.${String(scope.auth)} required for scoped query`);
|
|
331
|
+
}
|
|
332
|
+
return eq(col, val);
|
|
333
|
+
}
|
|
334
|
+
function drizzleTable(table, opts = {}) {
|
|
335
|
+
const adapter = drizzleAdapter(table, opts.pk);
|
|
336
|
+
const query = drizzleQueryFn(table, opts);
|
|
337
|
+
return defineTable(adapter, {
|
|
338
|
+
...opts,
|
|
339
|
+
query
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
var drizzleTxAtomic = {
|
|
343
|
+
async begin(db) {
|
|
344
|
+
const { sql } = loadDrizzleSync();
|
|
345
|
+
const handle = db;
|
|
346
|
+
if (typeof handle?.run !== "function") {
|
|
347
|
+
throw new Error("drizzleTxAtomic.begin requires a drizzle db with a run() method");
|
|
348
|
+
}
|
|
349
|
+
await Promise.resolve(handle.run(sql`BEGIN`));
|
|
350
|
+
},
|
|
351
|
+
async commit(db) {
|
|
352
|
+
const { sql } = loadDrizzleSync();
|
|
353
|
+
const handle = db;
|
|
354
|
+
if (typeof handle?.run !== "function") {
|
|
355
|
+
throw new Error("drizzleTxAtomic.commit requires a drizzle db with a run() method");
|
|
356
|
+
}
|
|
357
|
+
await Promise.resolve(handle.run(sql`COMMIT`));
|
|
358
|
+
},
|
|
359
|
+
async rollback(db) {
|
|
360
|
+
const { sql } = loadDrizzleSync();
|
|
361
|
+
const handle = db;
|
|
362
|
+
if (typeof handle?.run !== "function") {
|
|
363
|
+
throw new Error("drizzleTxAtomic.rollback requires a drizzle db with a run() method");
|
|
364
|
+
}
|
|
365
|
+
await Promise.resolve(handle.run(sql`ROLLBACK`));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export { defineTable, drizzleTable, drizzleTxAtomic };
|