@m1212e/rumble 0.24.2 → 0.25.2
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/out/client.cjs +54 -50
- package/out/client.cjs.map +1 -1
- package/out/client.d.cts.map +1 -1
- package/out/client.d.mts.map +1 -1
- package/out/client.mjs +54 -50
- package/out/client.mjs.map +1 -1
- package/out/index.cjs +835 -490
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +303 -284
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +300 -281
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +838 -496
- package/out/index.mjs.map +1 -1
- package/package.json +9 -8
package/out/index.mjs
CHANGED
|
@@ -7,15 +7,15 @@ import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspect
|
|
|
7
7
|
import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
|
|
8
8
|
import { capitalize, cloneDeep, debounce, merge, toMerged } from "es-toolkit";
|
|
9
9
|
import { createPubSub, createYoga } from "graphql-yoga";
|
|
10
|
-
import { One, count, getTableColumns, getTableName, is, isTable, relationsFilterToSQL, sql } from "drizzle-orm";
|
|
10
|
+
import { One, aliasedTable, count, getTableColumns, getTableName, is, isTable, or, relationsFilterToSQL, sql } from "drizzle-orm";
|
|
11
11
|
import { ATTR_GRAPHQL_DOCUMENT, ATTR_GRAPHQL_OPERATION_NAME, ATTR_GRAPHQL_OPERATION_TYPE } from "@opentelemetry/semantic-conventions/incubating";
|
|
12
12
|
import { AttributeNames, SpanNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
|
|
13
13
|
import { toCamelCase } from "drizzle-orm/casing";
|
|
14
|
-
import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, isPgEnum } from "drizzle-orm/pg-core";
|
|
15
|
-
import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable } from "drizzle-orm/mysql-core";
|
|
14
|
+
import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, getTableConfig, isPgEnum } from "drizzle-orm/pg-core";
|
|
15
|
+
import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable, getTableConfig as getTableConfig$1 } from "drizzle-orm/mysql-core";
|
|
16
16
|
import pluralize from "pluralize";
|
|
17
|
-
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
18
|
-
import SchemaBuilder, { BasePlugin } from "@pothos/core";
|
|
17
|
+
import { SQLiteTable, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
18
|
+
import SchemaBuilder, { BasePlugin, isThenable } from "@pothos/core";
|
|
19
19
|
import DrizzlePlugin from "@pothos/plugin-drizzle";
|
|
20
20
|
import SmartSubscriptionsPlugin, { subscribeOptionsFromIterator } from "@pothos/plugin-smart-subscriptions";
|
|
21
21
|
import TracingPlugin, { isRootField, wrapResolver } from "@pothos/plugin-tracing";
|
|
@@ -186,38 +186,76 @@ function mapNullFieldsToUndefined(obj) {
|
|
|
186
186
|
}
|
|
187
187
|
//#endregion
|
|
188
188
|
//#region package.json
|
|
189
|
-
var version = "0.
|
|
189
|
+
var version = "0.25.2";
|
|
190
190
|
//#endregion
|
|
191
|
-
//#region lib/helpers/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
191
|
+
//#region lib/helpers/tableHelpers.ts
|
|
192
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
function buildIndex(db) {
|
|
194
|
+
const byTsName = /* @__PURE__ */ new Map();
|
|
195
|
+
const byDbName = /* @__PURE__ */ new Map();
|
|
196
|
+
const byTableRef = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const [tsName, rawEntry] of Object.entries(db._.relations ?? {})) {
|
|
198
|
+
const entry = rawEntry;
|
|
199
|
+
const tableObj = entry.table;
|
|
200
|
+
if (!tableObj) continue;
|
|
201
|
+
const columns = isTable(tableObj) ? getTableColumns(tableObj) : {};
|
|
202
|
+
const primaryKey = Object.fromEntries(Object.entries(columns).filter(([, col]) => col.primary));
|
|
203
|
+
const dbName = isTable(tableObj) ? getTableName(tableObj) : entry.name ?? tsName;
|
|
204
|
+
const resolved = {
|
|
205
|
+
name: entry.name ?? tsName,
|
|
206
|
+
dbName,
|
|
207
|
+
table: tableObj,
|
|
208
|
+
columns,
|
|
209
|
+
primaryKey,
|
|
210
|
+
relations: entry.relations ?? {},
|
|
211
|
+
isTable: isTable(tableObj),
|
|
212
|
+
foundRelation: entry
|
|
204
213
|
};
|
|
214
|
+
byTsName.set(resolved.name, resolved);
|
|
215
|
+
byDbName.set(resolved.dbName, resolved);
|
|
216
|
+
byTableRef.set(tableObj, resolved);
|
|
205
217
|
}
|
|
206
218
|
return {
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
byTsName,
|
|
220
|
+
byDbName,
|
|
221
|
+
byTableRef
|
|
209
222
|
};
|
|
210
223
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return
|
|
224
|
+
function getIndex(db) {
|
|
225
|
+
let idx = indexCache.get(db);
|
|
226
|
+
if (!idx) {
|
|
227
|
+
idx = buildIndex(db);
|
|
228
|
+
indexCache.set(db, idx);
|
|
229
|
+
}
|
|
230
|
+
return idx;
|
|
218
231
|
}
|
|
219
|
-
function
|
|
220
|
-
|
|
232
|
+
function tableHelper({ db, table }) {
|
|
233
|
+
const idx = getIndex(db);
|
|
234
|
+
let entry;
|
|
235
|
+
if (typeof table === "string") entry = idx.byTsName.get(table) ?? idx.byDbName.get(table);
|
|
236
|
+
else if (table && typeof table === "object") {
|
|
237
|
+
const t = table;
|
|
238
|
+
if (t.table) {
|
|
239
|
+
entry = idx.byTableRef.get(t.table);
|
|
240
|
+
if (!entry && t.name) entry = idx.byTsName.get(t.name) ?? idx.byDbName.get(t.name);
|
|
241
|
+
}
|
|
242
|
+
if (!entry) entry = idx.byTableRef.get(t);
|
|
243
|
+
if (!entry && isTable(t)) {
|
|
244
|
+
const dbName = getTableName(t);
|
|
245
|
+
entry = idx.byDbName.get(dbName);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!entry) throw new RumbleError(`Could not find schema for ${JSON.stringify(typeof table === "string" ? table : "<table-object>")}`);
|
|
249
|
+
return {
|
|
250
|
+
columns: entry.columns,
|
|
251
|
+
primaryKey: entry.primaryKey,
|
|
252
|
+
relations: entry.relations,
|
|
253
|
+
dbName: entry.dbName,
|
|
254
|
+
tsName: entry.name,
|
|
255
|
+
table: entry.table,
|
|
256
|
+
isTable: entry.isTable,
|
|
257
|
+
foundRelation: entry.foundRelation
|
|
258
|
+
};
|
|
221
259
|
}
|
|
222
260
|
//#endregion
|
|
223
261
|
//#region lib/helpers/mergeFilters.ts
|
|
@@ -232,20 +270,36 @@ const EmptyFilter$1 = Symbol.for("drizzle:EmptyFilter");
|
|
|
232
270
|
function realWhere(where) {
|
|
233
271
|
return where === EmptyFilter$1 ? void 0 : where;
|
|
234
272
|
}
|
|
273
|
+
function mergeWhere(a, b, mode) {
|
|
274
|
+
if (a && b) return mode === "OR" ? { OR: [a, b] } : { AND: [a, b] };
|
|
275
|
+
return mode === "OR" ? void 0 : a ?? b;
|
|
276
|
+
}
|
|
277
|
+
function mergeColumns(a, b) {
|
|
278
|
+
if (!a && !b) return void 0;
|
|
279
|
+
const result = {};
|
|
280
|
+
for (const [key, value] of [...Object.entries(a ?? {}), ...Object.entries(b ?? {})]) if (value === true) result[key] = true;
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function mergeRecords(a, b) {
|
|
284
|
+
return a || b ? toMerged(a ?? {}, b ?? {}) : void 0;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Combines two limit/offset style bounds. For OR the looser bound wins (and a
|
|
288
|
+
* missing side means unbounded), for AND the stricter bound wins.
|
|
289
|
+
*/
|
|
290
|
+
function mergeBound(a, b, mode, orPick) {
|
|
291
|
+
if (mode === "OR") return a === void 0 || b === void 0 ? void 0 : orPick(a, b);
|
|
292
|
+
return a || b ? Math.min(a ?? Infinity, b ?? Infinity) : void 0;
|
|
293
|
+
}
|
|
235
294
|
function mergeFilters(filterA, filterB, mode = "AND") {
|
|
236
|
-
const filterAWhere = realWhere(filterA?.where);
|
|
237
|
-
const filterBWhere = realWhere(filterB?.where);
|
|
238
295
|
return {
|
|
239
|
-
where:
|
|
240
|
-
columns:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
limit: mode === "OR" ? filterA?.limit === void 0 || filterB?.limit === void 0 ? void 0 : Math.max(filterA.limit, filterB.limit) : filterA?.limit || filterB?.limit ? Math.min(filterA?.limit ?? Infinity, filterB?.limit ?? Infinity) : void 0,
|
|
247
|
-
offset: mode === "OR" ? filterA?.offset === void 0 || filterB?.offset === void 0 ? void 0 : Math.min(filterA.offset, filterB.offset) : filterA?.offset || filterB?.offset ? Math.min(filterA?.offset ?? Infinity, filterB?.offset ?? Infinity) : void 0,
|
|
248
|
-
with: filterA?.with || filterB?.with ? toMerged(filterA?.with ?? {}, filterB?.with ?? {}) : void 0
|
|
296
|
+
where: mergeWhere(realWhere(filterA?.where), realWhere(filterB?.where), mode),
|
|
297
|
+
columns: mergeColumns(filterA?.columns, filterB?.columns),
|
|
298
|
+
extras: mergeRecords(filterA?.extras, filterB?.extras),
|
|
299
|
+
orderBy: mergeRecords(filterA?.orderBy, filterB?.orderBy),
|
|
300
|
+
limit: mergeBound(filterA?.limit, filterB?.limit, mode, Math.max),
|
|
301
|
+
offset: mergeBound(filterA?.offset, filterB?.offset, mode, Math.min),
|
|
302
|
+
with: mergeRecords(filterA?.with, filterB?.with)
|
|
249
303
|
};
|
|
250
304
|
}
|
|
251
305
|
//#endregion
|
|
@@ -263,6 +317,201 @@ function sanitizeFilterValue(value) {
|
|
|
263
317
|
return value;
|
|
264
318
|
}
|
|
265
319
|
//#endregion
|
|
320
|
+
//#region lib/abilityBuilder/keys.ts
|
|
321
|
+
const ACTION_FLAG_KEY = "__rumble_action";
|
|
322
|
+
const columnFlagKey = (group) => `__rumble_columns_${group}`;
|
|
323
|
+
const columnMaskKey = Symbol.for("rumble:columnMask");
|
|
324
|
+
const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
|
|
325
|
+
const requestStateKey = Symbol.for("rumble:requestState");
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region lib/abilityBuilder/requestState.ts
|
|
328
|
+
function createRequestState() {
|
|
329
|
+
return {
|
|
330
|
+
masked: /* @__PURE__ */ new WeakMap(),
|
|
331
|
+
actions: /* @__PURE__ */ new Map(),
|
|
332
|
+
filterLoaders: /* @__PURE__ */ new Map()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function attachRequestState(abilities, state) {
|
|
336
|
+
Object.defineProperty(abilities, requestStateKey, { value: state });
|
|
337
|
+
}
|
|
338
|
+
function requestStateOf(abilities) {
|
|
339
|
+
return abilities?.[requestStateKey];
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region lib/abilityBuilder/columnMask.ts
|
|
343
|
+
function referencesRelation(filter, relations) {
|
|
344
|
+
if (!filter || typeof filter !== "object") return false;
|
|
345
|
+
return Object.entries(filter).some(([key, value]) => {
|
|
346
|
+
if (key === "AND" || key === "OR") return value.some((v) => referencesRelation(v, relations));
|
|
347
|
+
if (key === "NOT") return referencesRelation(value, relations);
|
|
348
|
+
return key in relations;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function selectedColumns(columns, allColumns) {
|
|
352
|
+
if (!columns) return new Set(allColumns);
|
|
353
|
+
const entries = Object.entries(columns).filter(([, v]) => v !== void 0);
|
|
354
|
+
if (entries.some(([, v]) => v)) return new Set(entries.filter(([, v]) => v).map(([k]) => k));
|
|
355
|
+
if (entries.length === 0) return /* @__PURE__ */ new Set();
|
|
356
|
+
return new Set(allColumns.filter((c) => columns[c] !== false));
|
|
357
|
+
}
|
|
358
|
+
function hiddenColumns(mask, matched, allColumns, primaryKey) {
|
|
359
|
+
let hidden = mask.hiddenPerMatch.get(matched);
|
|
360
|
+
if (!hidden) {
|
|
361
|
+
const columns = allColumns.filter((column) => !mask.guaranteed.has(column) && !mask.conditional.some((columns, group) => matched[group] === "1" && columns.has(column)));
|
|
362
|
+
hidden = {
|
|
363
|
+
cleared: columns.filter((c) => !primaryKey.includes(c)),
|
|
364
|
+
primaryKey: columns.some((c) => primaryKey.includes(c))
|
|
365
|
+
};
|
|
366
|
+
mask.hiddenPerMatch.set(matched, hidden);
|
|
367
|
+
}
|
|
368
|
+
return hidden;
|
|
369
|
+
}
|
|
370
|
+
const hasWhere = (where) => !!where && Object.keys(where).length > 0;
|
|
371
|
+
/** Columns every returned row is allowed to expose. */
|
|
372
|
+
function guaranteedColumns(rules, allColumnNames) {
|
|
373
|
+
const guaranteed = /* @__PURE__ */ new Set();
|
|
374
|
+
for (const rule of rules) {
|
|
375
|
+
if (hasWhere(rule.where)) continue;
|
|
376
|
+
for (const column of rule.columns) guaranteed.add(column);
|
|
377
|
+
}
|
|
378
|
+
for (const column of allColumnNames) if (rules.every((r) => r.columns.has(column))) guaranteed.add(column);
|
|
379
|
+
return guaranteed;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Groups the conditional rules by the columns they grant on top of the
|
|
383
|
+
* guaranteed ones. Each group gets its own flag.
|
|
384
|
+
*/
|
|
385
|
+
function conditionalGroups(rules, guaranteed) {
|
|
386
|
+
const groups = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const rule of rules) {
|
|
388
|
+
if (!hasWhere(rule.where)) continue;
|
|
389
|
+
const additional = [...rule.columns].filter((c) => !guaranteed.has(c)).sort();
|
|
390
|
+
if (additional.length === 0) continue;
|
|
391
|
+
const key = additional.join(",");
|
|
392
|
+
const group = groups.get(key);
|
|
393
|
+
if (group) group.wheres.push(rule.where);
|
|
394
|
+
else groups.set(key, {
|
|
395
|
+
columns: new Set(additional),
|
|
396
|
+
wheres: [rule.where]
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return [...groups.values()];
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Creates the resolver which turns the abilities of a table into the columns
|
|
403
|
+
* to select and, if columns depend on the matched ability, extras flagging
|
|
404
|
+
* per row which abilities matched plus the mask to decode them.
|
|
405
|
+
*/
|
|
406
|
+
function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
407
|
+
const allColumnNames = Object.keys(tableSchema.columns);
|
|
408
|
+
const primaryKeyNames = Object.keys(tableSchema.primaryKey);
|
|
409
|
+
const primaryKeyName = primaryKeyNames[0];
|
|
410
|
+
const toSQL = (table, wheres) => or(...wheres.map((where) => relationsFilterToSQL(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
|
|
411
|
+
const uncorrelated = (table, wheres, group) => {
|
|
412
|
+
const inner = aliasedTable(tableSchema.table, `rumble_columns_${group}`);
|
|
413
|
+
return sql`${table[primaryKeyName]} in (${db.select({ pk: inner[primaryKeyName] }).from(inner).where(toSQL(inner, wheres))})`;
|
|
414
|
+
};
|
|
415
|
+
return function resolveColumnAccess(action, filters) {
|
|
416
|
+
const rules = filters.map((f) => ({
|
|
417
|
+
where: realWhere(f?.where),
|
|
418
|
+
columns: selectedColumns(f?.columns, allColumnNames)
|
|
419
|
+
}));
|
|
420
|
+
const guaranteed = guaranteedColumns(rules, allColumnNames);
|
|
421
|
+
const groups = conditionalGroups(rules, guaranteed);
|
|
422
|
+
const selected = /* @__PURE__ */ new Set([...primaryKeyNames, ...rules.flatMap((r) => [...r.columns])]);
|
|
423
|
+
const columns = selected.size === allColumnNames.length ? void 0 : Object.fromEntries([...selected].map((c) => [c, true]));
|
|
424
|
+
if (guaranteed.size === allColumnNames.length) return {
|
|
425
|
+
columns,
|
|
426
|
+
extras: void 0,
|
|
427
|
+
mask: void 0
|
|
428
|
+
};
|
|
429
|
+
const mask = {
|
|
430
|
+
guaranteed,
|
|
431
|
+
conditional: groups.map((g) => g.columns),
|
|
432
|
+
flags: groups.map((_, group) => columnFlagKey(group)),
|
|
433
|
+
hiddenPerMatch: /* @__PURE__ */ new Map()
|
|
434
|
+
};
|
|
435
|
+
const actionIndex = sql.raw(String(actions.indexOf(action)));
|
|
436
|
+
const extras = { [ACTION_FLAG_KEY]: () => sql`${actionIndex}` };
|
|
437
|
+
groups.forEach(({ wheres }, group) => {
|
|
438
|
+
const relational = wheres.some((w) => referencesRelation(w, tableSchema.relations));
|
|
439
|
+
extras[mask.flags[group]] = (table) => {
|
|
440
|
+
const condition = relational ? uncorrelated(table, wheres, group) : toSQL(table, wheres);
|
|
441
|
+
return sql`case when ${condition} then 1 else 0 end`;
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
columns,
|
|
446
|
+
extras,
|
|
447
|
+
mask
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const isRow = (row) => !!row && typeof row === "object";
|
|
452
|
+
function clearColumns(row, columns) {
|
|
453
|
+
for (let i = 0; i < columns.length; i++) {
|
|
454
|
+
const column = columns[i];
|
|
455
|
+
if (row[column] !== void 0) row[column] = void 0;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function matchedGroups(row, flags) {
|
|
459
|
+
let matched = "";
|
|
460
|
+
for (let i = 0; i < flags.length; i++) matched += Number(row[flags[i]]) === 1 ? "1" : "0";
|
|
461
|
+
return matched;
|
|
462
|
+
}
|
|
463
|
+
function maskRow(row, mask, hidden) {
|
|
464
|
+
if (row["__rumble_action"] !== void 0) row[ACTION_FLAG_KEY] = void 0;
|
|
465
|
+
clearColumns(row, mask.flags);
|
|
466
|
+
clearColumns(row, hidden.cleared);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Creates the function hiding the columns of queried rows which the ability
|
|
470
|
+
* that matched the row does not grant.
|
|
471
|
+
*/
|
|
472
|
+
function createColumnMasker({ actions }, tables) {
|
|
473
|
+
const stateOf = (abilities) => requestStateOf(abilities);
|
|
474
|
+
const maskedActions = (abilities, table) => {
|
|
475
|
+
const { actions } = stateOf(abilities);
|
|
476
|
+
let used = actions.get(table);
|
|
477
|
+
if (!used) {
|
|
478
|
+
used = /* @__PURE__ */ new Set();
|
|
479
|
+
actions.set(table, used);
|
|
480
|
+
}
|
|
481
|
+
return used;
|
|
482
|
+
};
|
|
483
|
+
const primaryKeyHidden = (abilities, row) => stateOf(abilities).masked.get(row) === true;
|
|
484
|
+
const maskColumns = ({ table, action, abilities, entities }, start = 0) => {
|
|
485
|
+
const tableAbilities = abilities[table];
|
|
486
|
+
const { columns: columnNames, primaryKey } = tables.get(table);
|
|
487
|
+
const state = stateOf(abilities);
|
|
488
|
+
const used = maskedActions(abilities, table);
|
|
489
|
+
for (let i = start; i < entities.length; i++) {
|
|
490
|
+
const row = entities[i];
|
|
491
|
+
if (!isRow(row) || state.masked.has(row)) continue;
|
|
492
|
+
const rowAction = row["__rumble_action"] != null ? actions[Number(row[ACTION_FLAG_KEY])] : action;
|
|
493
|
+
used.add(rowAction);
|
|
494
|
+
const resolved = tableAbilities[resolvedFilterKey](rowAction);
|
|
495
|
+
if (!resolved) return tableAbilities.filter(rowAction).then(() => maskColumns({
|
|
496
|
+
table,
|
|
497
|
+
action,
|
|
498
|
+
abilities,
|
|
499
|
+
entities
|
|
500
|
+
}, i));
|
|
501
|
+
const mask = resolved[columnMaskKey];
|
|
502
|
+
if (!mask) continue;
|
|
503
|
+
const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
|
|
504
|
+
maskRow(row, mask, hidden);
|
|
505
|
+
state.masked.set(row, hidden.primaryKey);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
maskColumns,
|
|
510
|
+
maskedActions,
|
|
511
|
+
primaryKeyHidden
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
266
515
|
//#region lib/helpers/sqlTypes/types.ts
|
|
267
516
|
/**
|
|
268
517
|
* Strips parameter suffixes like `(256)`, `(10,2)` and timezone modifiers
|
|
@@ -408,76 +657,6 @@ function createDistinctValuesFromSQLType(sqlType) {
|
|
|
408
657
|
throw UnknownTypeRumbleError(sqlType, "Distinct");
|
|
409
658
|
}
|
|
410
659
|
//#endregion
|
|
411
|
-
//#region lib/helpers/tableHelpers.ts
|
|
412
|
-
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
413
|
-
function buildIndex(db) {
|
|
414
|
-
const byTsName = /* @__PURE__ */ new Map();
|
|
415
|
-
const byDbName = /* @__PURE__ */ new Map();
|
|
416
|
-
const byTableRef = /* @__PURE__ */ new Map();
|
|
417
|
-
for (const [tsName, rawEntry] of Object.entries(db._.relations ?? {})) {
|
|
418
|
-
const entry = rawEntry;
|
|
419
|
-
const tableObj = entry.table;
|
|
420
|
-
if (!tableObj) continue;
|
|
421
|
-
const columns = isTable(tableObj) ? getTableColumns(tableObj) : {};
|
|
422
|
-
const primaryKey = Object.fromEntries(Object.entries(columns).filter(([, col]) => col.primary));
|
|
423
|
-
const dbName = isTable(tableObj) ? getTableName(tableObj) : entry.name ?? tsName;
|
|
424
|
-
const resolved = {
|
|
425
|
-
name: entry.name ?? tsName,
|
|
426
|
-
dbName,
|
|
427
|
-
table: tableObj,
|
|
428
|
-
columns,
|
|
429
|
-
primaryKey,
|
|
430
|
-
relations: entry.relations ?? {},
|
|
431
|
-
isTable: isTable(tableObj),
|
|
432
|
-
foundRelation: entry
|
|
433
|
-
};
|
|
434
|
-
byTsName.set(resolved.name, resolved);
|
|
435
|
-
byDbName.set(resolved.dbName, resolved);
|
|
436
|
-
byTableRef.set(tableObj, resolved);
|
|
437
|
-
}
|
|
438
|
-
return {
|
|
439
|
-
byTsName,
|
|
440
|
-
byDbName,
|
|
441
|
-
byTableRef
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
function getIndex(db) {
|
|
445
|
-
let idx = indexCache.get(db);
|
|
446
|
-
if (!idx) {
|
|
447
|
-
idx = buildIndex(db);
|
|
448
|
-
indexCache.set(db, idx);
|
|
449
|
-
}
|
|
450
|
-
return idx;
|
|
451
|
-
}
|
|
452
|
-
function tableHelper({ db, table }) {
|
|
453
|
-
const idx = getIndex(db);
|
|
454
|
-
let entry;
|
|
455
|
-
if (typeof table === "string") entry = idx.byTsName.get(table) ?? idx.byDbName.get(table);
|
|
456
|
-
else if (table && typeof table === "object") {
|
|
457
|
-
const t = table;
|
|
458
|
-
if (t.table) {
|
|
459
|
-
entry = idx.byTableRef.get(t.table);
|
|
460
|
-
if (!entry && t.name) entry = idx.byTsName.get(t.name) ?? idx.byDbName.get(t.name);
|
|
461
|
-
}
|
|
462
|
-
if (!entry) entry = idx.byTableRef.get(t);
|
|
463
|
-
if (!entry && isTable(t)) {
|
|
464
|
-
const dbName = getTableName(t);
|
|
465
|
-
entry = idx.byDbName.get(dbName);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (!entry) throw new RumbleError(`Could not find schema for ${JSON.stringify(typeof table === "string" ? table : "<table-object>")}`);
|
|
469
|
-
return {
|
|
470
|
-
columns: entry.columns,
|
|
471
|
-
primaryKey: entry.primaryKey,
|
|
472
|
-
relations: entry.relations,
|
|
473
|
-
dbName: entry.dbName,
|
|
474
|
-
tsName: entry.name,
|
|
475
|
-
table: entry.table,
|
|
476
|
-
isTable: entry.isTable,
|
|
477
|
-
foundRelation: entry.foundRelation
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
//#endregion
|
|
481
660
|
//#region lib/helpers/telemetry.ts
|
|
482
661
|
const SPAN_EXECUTE = SpanNames.EXECUTE;
|
|
483
662
|
/** @pothos/tracing-opentelemetry's SpanNames enum has no subscribe member. */
|
|
@@ -653,13 +832,161 @@ function durationMs(start) {
|
|
|
653
832
|
return Math.round((performance.now() - start) * 100) / 100;
|
|
654
833
|
}
|
|
655
834
|
//#endregion
|
|
656
|
-
//#region lib/abilityBuilder.ts
|
|
835
|
+
//#region lib/abilityBuilder/filterResponse.ts
|
|
836
|
+
/**
|
|
837
|
+
* The limit of a find many call. An injected merge limit wins unless the
|
|
838
|
+
* ability limit is lower, otherwise the default limit caps the ability limit.
|
|
839
|
+
*/
|
|
840
|
+
function resolveLimit(abilityLimit, mergedLimit, defaultLimit) {
|
|
841
|
+
if (mergedLimit !== void 0 && (!abilityLimit || abilityLimit > mergedLimit)) return mergedLimit;
|
|
842
|
+
if (defaultLimit && (abilityLimit === void 0 || abilityLimit > defaultLimit)) return defaultLimit;
|
|
843
|
+
return abilityLimit ?? void 0;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Creates the function packing resolved query filters into the object handed
|
|
847
|
+
* out by `abilities.<table>.filter(...)`.
|
|
848
|
+
*/
|
|
849
|
+
function createFilterResponder({ db, defaultLimit }, tableSchema) {
|
|
850
|
+
const internalTransformer = (filters, mergedLimit) => {
|
|
851
|
+
const where = filters?.where;
|
|
852
|
+
const extras = filters?.extras;
|
|
853
|
+
const columns = filters?.columns;
|
|
854
|
+
const limit = lazy(() => resolveLimit(filters?.limit, mergedLimit, defaultLimit));
|
|
855
|
+
const sqlTransformedWhere = lazy(() => {
|
|
856
|
+
const casing = db._.session?.dialect?.casing;
|
|
857
|
+
return where ? relationsFilterToSQL(tableSchema.foundRelation.table, sanitizeFilterValue(where), tableSchema.relations, db._.relations, casing) : void 0;
|
|
858
|
+
});
|
|
859
|
+
return {
|
|
860
|
+
query: {
|
|
861
|
+
single: columns ? {
|
|
862
|
+
extras,
|
|
863
|
+
where: sanitizeFilterValue(where),
|
|
864
|
+
columns
|
|
865
|
+
} : {
|
|
866
|
+
extras,
|
|
867
|
+
where: sanitizeFilterValue(where)
|
|
868
|
+
},
|
|
869
|
+
many: columns ? {
|
|
870
|
+
extras,
|
|
871
|
+
where: sanitizeFilterValue(where),
|
|
872
|
+
columns,
|
|
873
|
+
get limit() {
|
|
874
|
+
return limit();
|
|
875
|
+
}
|
|
876
|
+
} : {
|
|
877
|
+
extras,
|
|
878
|
+
where: sanitizeFilterValue(where),
|
|
879
|
+
get limit() {
|
|
880
|
+
return limit();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
sql: { get where() {
|
|
885
|
+
return sqlTransformedWhere();
|
|
886
|
+
} }
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
/**
|
|
890
|
+
* Packs the filters into a response object that can be applied for queries by the user
|
|
891
|
+
*/
|
|
892
|
+
return function transformToResponse(queryFilters, columnMask) {
|
|
893
|
+
const ret = internalTransformer(queryFilters);
|
|
894
|
+
ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
|
|
895
|
+
ret[columnMaskKey] = columnMask;
|
|
896
|
+
return ret;
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region lib/abilityBuilder/queryFilter.ts
|
|
657
901
|
function isDynamicQueryFilter(filter) {
|
|
658
902
|
return typeof filter === "function";
|
|
659
903
|
}
|
|
660
904
|
function isStaticQueryFilter(filter) {
|
|
661
905
|
return typeof filter !== "function";
|
|
662
906
|
}
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region lib/helpers/errorLogging.ts
|
|
909
|
+
function hasMessage(error) {
|
|
910
|
+
return typeof error === "object" && error !== null && "message" in error;
|
|
911
|
+
}
|
|
912
|
+
function toLoggedException(error) {
|
|
913
|
+
if (hasMessage(error)) {
|
|
914
|
+
const gqlError = error;
|
|
915
|
+
return {
|
|
916
|
+
"exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
|
|
917
|
+
"exception.message": String(gqlError.message),
|
|
918
|
+
...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
|
|
919
|
+
...gqlError.path ? { path: gqlError.path } : {},
|
|
920
|
+
...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
"exception.type": "UnknownError",
|
|
925
|
+
"exception.message": String(error)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Every erroneous rumble log carries the same `errors` array shape,
|
|
930
|
+
* whether it originates from a thrown exception (single-element array)
|
|
931
|
+
* or a GraphQL `ExecutionResult.errors` list (multi-element array).
|
|
932
|
+
*/
|
|
933
|
+
function errorsLogField(errors) {
|
|
934
|
+
return { errors: errors.map(toLoggedException) };
|
|
935
|
+
}
|
|
936
|
+
function errorLogField(error) {
|
|
937
|
+
return errorsLogField([error]);
|
|
938
|
+
}
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region lib/abilityBuilder/telemetry.ts
|
|
941
|
+
const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
|
|
942
|
+
const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
|
|
943
|
+
if (logger) logger.warn({
|
|
944
|
+
[ATTR_TABLE]: model,
|
|
945
|
+
[ATTR_ACTION]: action,
|
|
946
|
+
...traceCorrelationFields(config)
|
|
947
|
+
}, msg);
|
|
948
|
+
else console.warn(msg);
|
|
949
|
+
}, 1e3);
|
|
950
|
+
/**
|
|
951
|
+
* Runs the preparation of the abilities of one table and action, reporting
|
|
952
|
+
* the attributes it collects to the span and the logger.
|
|
953
|
+
*/
|
|
954
|
+
function traceAbilityPreparation({ otel, log, telemetryConfig }, tableName, action, assemble) {
|
|
955
|
+
const run = async (span) => {
|
|
956
|
+
const attributes = {
|
|
957
|
+
[ATTR_TABLE]: tableName,
|
|
958
|
+
[ATTR_ACTION]: action
|
|
959
|
+
};
|
|
960
|
+
try {
|
|
961
|
+
const result = await assemble(attributes);
|
|
962
|
+
log?.debug({
|
|
963
|
+
...attributes,
|
|
964
|
+
...traceCorrelationFields(telemetryConfig, span)
|
|
965
|
+
}, "abilities prepared");
|
|
966
|
+
return result;
|
|
967
|
+
} catch (error) {
|
|
968
|
+
recordSpanError(span, error);
|
|
969
|
+
log?.error({
|
|
970
|
+
...attributes,
|
|
971
|
+
...traceCorrelationFields(telemetryConfig, span),
|
|
972
|
+
...errorLogField(error)
|
|
973
|
+
}, "abilities failed");
|
|
974
|
+
throw error;
|
|
975
|
+
} finally {
|
|
976
|
+
span?.setAttributes(attributes);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
|
|
980
|
+
try {
|
|
981
|
+
return await run(span);
|
|
982
|
+
} finally {
|
|
983
|
+
span.end();
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
return run();
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region lib/abilityBuilder/tableAbilities.ts
|
|
663
990
|
/**
|
|
664
991
|
* filter() is async. Without type checking (plain JS, `any`) a forgotten await would
|
|
665
992
|
* hand the promise itself to drizzle
|
|
@@ -677,114 +1004,223 @@ function guardAgainstMissingAwait(promise) {
|
|
|
677
1004
|
});
|
|
678
1005
|
return promise;
|
|
679
1006
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
},
|
|
1007
|
+
/**
|
|
1008
|
+
* A filter no row can match, since it requires the primary key to equal two
|
|
1009
|
+
* distinct values at once.
|
|
1010
|
+
*/
|
|
1011
|
+
function createBlockEverythingFilter(tableName, tableSchema) {
|
|
1012
|
+
if (Object.keys(tableSchema.primaryKey).length === 0) throw new RumbleError(`No primary key found for entity ${tableName}`);
|
|
1013
|
+
const [primaryKeyName, primaryKeyField] = Object.entries(tableSchema.primaryKey)[0];
|
|
1014
|
+
const distinctValues = createDistinctValuesFromSQLType(primaryKeyField.getSQLType());
|
|
1015
|
+
return { where: { AND: [{ [primaryKeyName]: distinctValues.value1 }, { [primaryKeyName]: distinctValues.value2 }] } };
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Creates the abilities of a single table, which resolve the registered
|
|
1019
|
+
* query filters for a request.
|
|
1020
|
+
*/
|
|
1021
|
+
const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
1022
|
+
const { db, actions } = settings;
|
|
1023
|
+
const tableSchema = tableHelper({
|
|
1024
|
+
db,
|
|
1025
|
+
table: tableName
|
|
1026
|
+
});
|
|
1027
|
+
const withRestrictingWhere = (filter) => {
|
|
1028
|
+
const where = realWhere(filter.where);
|
|
1029
|
+
return !!where && Object.keys(where).length > 0 && relationsFilterToSQL(tableSchema.table, sanitizeFilterValue(where), tableSchema.relations, db._.relations) !== void 0 || where === void 0 ? filter : {
|
|
1030
|
+
...filter,
|
|
1031
|
+
where: void 0
|
|
1032
|
+
};
|
|
1033
|
+
};
|
|
1034
|
+
const simpleQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
1035
|
+
const filters = queryFilters.get(action);
|
|
1036
|
+
if (!filters || filters === "unrestricted") return [action, []];
|
|
1037
|
+
return [action, filters.filter(isStaticQueryFilter).map(withRestrictingWhere)];
|
|
1038
|
+
}));
|
|
1039
|
+
const dynamicQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
1040
|
+
const filters = queryFilters.get(action);
|
|
1041
|
+
if (!filters || filters === "unrestricted") return [action, []];
|
|
1042
|
+
return [action, filters.filter(isDynamicQueryFilter)];
|
|
1043
|
+
}));
|
|
1044
|
+
const blockEverythingFilter = createBlockEverythingFilter(String(tableName), tableSchema);
|
|
1045
|
+
const resolveColumnAccess = createColumnAccessResolver(settings, tableSchema);
|
|
1046
|
+
const transformToResponse = createFilterResponder(settings, tableSchema);
|
|
1047
|
+
const assembleAbilities = async (userContext, action, attributes) => {
|
|
1048
|
+
const filters = queryFilters.get(action);
|
|
1049
|
+
if (filters === "unrestricted") {
|
|
1050
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1051
|
+
return transformToResponse();
|
|
1052
|
+
}
|
|
1053
|
+
if (!filters) {
|
|
1054
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1055
|
+
settings.warnNothingRegistered(String(tableName), action);
|
|
1056
|
+
return transformToResponse(blockEverythingFilter);
|
|
1057
|
+
}
|
|
1058
|
+
const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
|
|
1059
|
+
if (rawResults.includes("allow")) {
|
|
1060
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1061
|
+
return transformToResponse();
|
|
1062
|
+
}
|
|
1063
|
+
const dynamicResults = rawResults.filter((r) => r !== void 0).map(withRestrictingWhere);
|
|
1064
|
+
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
1065
|
+
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
1066
|
+
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
1067
|
+
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
1068
|
+
if (allQueryFilters.length === 0) {
|
|
1069
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1070
|
+
return transformToResponse(blockEverythingFilter);
|
|
1071
|
+
}
|
|
1072
|
+
const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
|
|
1073
|
+
return mergeFilters(a, b, "OR");
|
|
1074
|
+
});
|
|
1075
|
+
const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
|
|
1076
|
+
if (columns) mergedFilters.columns = columns;
|
|
1077
|
+
else delete mergedFilters.columns;
|
|
1078
|
+
if (extras) mergedFilters.extras = {
|
|
1079
|
+
...mergedFilters.extras,
|
|
1080
|
+
...extras
|
|
1081
|
+
};
|
|
1082
|
+
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
1083
|
+
return transformToResponse(mergedFilters, mask);
|
|
1084
|
+
};
|
|
1085
|
+
return { withContext: (userContext) => {
|
|
1086
|
+
const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
|
|
1087
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1088
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1089
|
+
const abilities = { filter: (action) => {
|
|
1090
|
+
let prepared = cache.get(action);
|
|
1091
|
+
if (!prepared) {
|
|
1092
|
+
prepared = guardAgainstMissingAwait(prepare(action));
|
|
1093
|
+
cache.set(action, prepared);
|
|
1094
|
+
prepared.then((result) => resolved.set(action, result), () => {});
|
|
1095
|
+
}
|
|
1096
|
+
return prepared;
|
|
1097
|
+
} };
|
|
1098
|
+
abilities[resolvedFilterKey] = (action) => resolved.get(action);
|
|
1099
|
+
return abilities;
|
|
1100
|
+
} };
|
|
1101
|
+
};
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region lib/abilityBuilder/tableBuilder.ts
|
|
1104
|
+
/**
|
|
1105
|
+
* Creates the registration API (allow/filter) of a single table.
|
|
1106
|
+
*/
|
|
1107
|
+
const createTableBuilder = ({ actions, isBuilt }) => {
|
|
1108
|
+
const queryFilters = /* @__PURE__ */ new Map();
|
|
1109
|
+
const runtimeFilters = /* @__PURE__ */ new Map();
|
|
1110
|
+
for (const action of actions) if (!runtimeFilters.has(action)) runtimeFilters.set(action, []);
|
|
1111
|
+
const registerFilter = (actions, prefetch) => ({
|
|
1112
|
+
/**
|
|
1113
|
+
* The actual filter function to apply. Returns the allowed values
|
|
1114
|
+
*/
|
|
1115
|
+
by: (explicitFilter) => {
|
|
1116
|
+
for (const action of actions) runtimeFilters.get(action).push(prefetch ? {
|
|
1117
|
+
filter: explicitFilter,
|
|
1118
|
+
prefetch
|
|
1119
|
+
} : { filter: explicitFilter });
|
|
1120
|
+
} });
|
|
1121
|
+
return {
|
|
1122
|
+
/**
|
|
1123
|
+
* Allows to perform a specific action on a specific entity
|
|
1124
|
+
*/
|
|
1125
|
+
allow: (action) => {
|
|
1126
|
+
if (isBuilt()) throw new RumbleError("You can't call allow() after the ability builder has been built. Please ensure that you register all abilities before accessing them.");
|
|
1127
|
+
const actions = Array.isArray(action) ? action : [action];
|
|
1128
|
+
const setUnrestrictedByThisCall = /* @__PURE__ */ new Set();
|
|
1129
|
+
for (const action of actions) {
|
|
1130
|
+
let filters = queryFilters.get(action);
|
|
1131
|
+
if (!filters) {
|
|
1132
|
+
filters = "unrestricted";
|
|
1133
|
+
queryFilters.set(action, filters);
|
|
1134
|
+
setUnrestrictedByThisCall.add(action);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
/**
|
|
1139
|
+
* Restricts the allowed actions to a filter
|
|
1140
|
+
* @example
|
|
1141
|
+
* ```ts
|
|
1142
|
+
* abilityBuilder.users.allow(["read", "update", "delete"]).when(({ userId }) => ({
|
|
1143
|
+
* where: {
|
|
1144
|
+
* id: userId,
|
|
1145
|
+
* },
|
|
1146
|
+
* }));
|
|
1147
|
+
* ```
|
|
1148
|
+
*/
|
|
1149
|
+
when: (queryFilter) => {
|
|
1150
|
+
for (const action of actions) {
|
|
1151
|
+
if (queryFilters.get(action) === "unrestricted") {
|
|
1152
|
+
if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
|
|
1153
|
+
else continue;
|
|
1154
|
+
}
|
|
1155
|
+
queryFilters.get(action).push(queryFilter);
|
|
1156
|
+
}
|
|
1157
|
+
} };
|
|
1158
|
+
},
|
|
1159
|
+
/**
|
|
1160
|
+
* Allows to register an application level filter to restrict some results
|
|
1161
|
+
* which were returned by a query
|
|
1162
|
+
*/
|
|
1163
|
+
filter: (action) => {
|
|
1164
|
+
const actions = Array.isArray(action) ? action : [action];
|
|
1165
|
+
return {
|
|
1166
|
+
/**
|
|
1167
|
+
* Allows to register an application level prefetch to fetch some data
|
|
1168
|
+
* which could be useful for later filtering the results. The prefetch
|
|
1169
|
+
* function will be called with the user context but unlike the actual
|
|
1170
|
+
* filter function, it will not have access to the result of the query
|
|
1171
|
+
* and therefore can be run in parallel with underlying query resolver.
|
|
1172
|
+
* A typical use case is to fetch some data which is not directly
|
|
1173
|
+
* related to the query but to the context only. So e.g. fetching the
|
|
1174
|
+
* user's permissions from an external system and then later applying
|
|
1175
|
+
* the filter based on those permissions.
|
|
1176
|
+
*/
|
|
1177
|
+
prefetch: (prefetch) => registerFilter(actions, prefetch),
|
|
1178
|
+
...registerFilter(actions)
|
|
1179
|
+
};
|
|
1180
|
+
},
|
|
1181
|
+
_: {
|
|
1182
|
+
runtimeFilters,
|
|
1183
|
+
queryFilters
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region lib/abilityBuilder/index.ts
|
|
689
1189
|
const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
|
|
690
1190
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
691
1191
|
const telemetryConfig = {
|
|
692
1192
|
otel,
|
|
693
1193
|
logger: loggerConfig
|
|
694
1194
|
};
|
|
695
|
-
const
|
|
1195
|
+
const settings = {
|
|
1196
|
+
db,
|
|
1197
|
+
actions,
|
|
1198
|
+
defaultLimit,
|
|
1199
|
+
otel,
|
|
1200
|
+
telemetryConfig,
|
|
1201
|
+
log,
|
|
1202
|
+
warnNothingRegistered: makeNothingRegisteredWarner(telemetryConfig, log)
|
|
1203
|
+
};
|
|
696
1204
|
const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
|
|
697
1205
|
db,
|
|
698
1206
|
table: name
|
|
699
1207
|
}).isTable);
|
|
700
1208
|
let hasBeenBuilt = false;
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
filters = "unrestricted";
|
|
717
|
-
queryFilters.set(action, filters);
|
|
718
|
-
setUnrestrictedByThisCall.add(action);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
/**
|
|
723
|
-
* Restricts the allowed actions to a filter
|
|
724
|
-
* @example
|
|
725
|
-
* ```ts
|
|
726
|
-
* abilityBuilder.users.allow(["read", "update", "delete"]).when(({ userId }) => ({
|
|
727
|
-
* where: {
|
|
728
|
-
* id: userId,
|
|
729
|
-
* },
|
|
730
|
-
* }));
|
|
731
|
-
* ```
|
|
732
|
-
*/
|
|
733
|
-
when: (queryFilter) => {
|
|
734
|
-
for (const action of actions) {
|
|
735
|
-
if (queryFilters.get(action) === "unrestricted") {
|
|
736
|
-
if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
|
|
737
|
-
else continue;
|
|
738
|
-
}
|
|
739
|
-
queryFilters.get(action).push(queryFilter);
|
|
740
|
-
}
|
|
741
|
-
} };
|
|
742
|
-
},
|
|
743
|
-
/**
|
|
744
|
-
* Allows to register an application level filter to restrict some results
|
|
745
|
-
* which were returned by a query
|
|
746
|
-
*/
|
|
747
|
-
filter: (action) => {
|
|
748
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
749
|
-
return {
|
|
750
|
-
/**
|
|
751
|
-
* Allows to register an application level prefetch to fetch some data
|
|
752
|
-
* which could be useful for later filtering the results. The prefetch
|
|
753
|
-
* function will be called with the user context but unlike the actual
|
|
754
|
-
* filter function, it will not have access to the result of the query
|
|
755
|
-
* and therefore can be run in parallel with underlying query resolver.
|
|
756
|
-
* A typical use case is to fetch some data which is not directly
|
|
757
|
-
* related to the query but to the context only. So e.g. fetching the
|
|
758
|
-
* user's permissions from an external system and then later applying
|
|
759
|
-
* the filter based on those permissions.
|
|
760
|
-
*/
|
|
761
|
-
prefetch: (prefetch) => {
|
|
762
|
-
return {
|
|
763
|
-
/**
|
|
764
|
-
* The actual filter function to apply. Returns the allowed values
|
|
765
|
-
*/
|
|
766
|
-
by: (explicitFilter) => {
|
|
767
|
-
for (const action of actions) runtimeFilters.get(action).push({
|
|
768
|
-
filter: explicitFilter,
|
|
769
|
-
prefetch
|
|
770
|
-
});
|
|
771
|
-
} };
|
|
772
|
-
},
|
|
773
|
-
/**
|
|
774
|
-
* The actual filter function to apply. Returns the allowed values
|
|
775
|
-
*/
|
|
776
|
-
by: (explicitFilter) => {
|
|
777
|
-
for (const action of actions) runtimeFilters.get(action).push({ filter: explicitFilter });
|
|
778
|
-
}
|
|
779
|
-
};
|
|
780
|
-
},
|
|
781
|
-
_: {
|
|
782
|
-
runtimeFilters,
|
|
783
|
-
queryFilters
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
};
|
|
787
|
-
const buildersPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createBuilderForTable()]));
|
|
1209
|
+
const buildersPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createTableBuilder({
|
|
1210
|
+
actions: settings.actions,
|
|
1211
|
+
isBuilt: () => hasBeenBuilt
|
|
1212
|
+
})]));
|
|
1213
|
+
const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1214
|
+
const tableSchema = tableHelper({
|
|
1215
|
+
db,
|
|
1216
|
+
table: tableName
|
|
1217
|
+
});
|
|
1218
|
+
return [tableName, {
|
|
1219
|
+
columns: Object.keys(tableSchema.columns),
|
|
1220
|
+
primaryKey: Object.keys(tableSchema.primaryKey)
|
|
1221
|
+
}];
|
|
1222
|
+
})));
|
|
1223
|
+
const readActionPerTable = /* @__PURE__ */ new Map();
|
|
788
1224
|
return {
|
|
789
1225
|
...buildersPerTable,
|
|
790
1226
|
/**
|
|
@@ -792,247 +1228,28 @@ by: (explicitFilter) => {
|
|
|
792
1228
|
* @ignore
|
|
793
1229
|
*/
|
|
794
1230
|
_: {
|
|
1231
|
+
maskColumns: (input) => maskColumns(input),
|
|
1232
|
+
/** The actions rows of the table were masked with in this request. */
|
|
1233
|
+
maskedActions: (abilities, table) => maskedActions(abilities, table),
|
|
1234
|
+
/** Whether masking in this request hid the primary key of the row. */
|
|
1235
|
+
primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
|
|
1236
|
+
registerReadAction(table, action) {
|
|
1237
|
+
readActionPerTable.set(table, action);
|
|
1238
|
+
},
|
|
1239
|
+
readActionOf(table) {
|
|
1240
|
+
return readActionPerTable.get(table) ?? "read";
|
|
1241
|
+
},
|
|
795
1242
|
registeredFilters({ action, table }) {
|
|
796
1243
|
return buildersPerTable[table]._.runtimeFilters.get(action);
|
|
797
1244
|
},
|
|
798
1245
|
build() {
|
|
799
|
-
const
|
|
800
|
-
const queryFilters = buildersPerTable[tableName]._.queryFilters;
|
|
801
|
-
const simpleQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
802
|
-
const filters = queryFilters.get(action);
|
|
803
|
-
if (!filters || filters === "unrestricted") return [action, []];
|
|
804
|
-
return [action, filters.filter(isStaticQueryFilter)];
|
|
805
|
-
}));
|
|
806
|
-
const dynamicQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
807
|
-
const filters = queryFilters.get(action);
|
|
808
|
-
if (!filters || filters === "unrestricted") return [action, []];
|
|
809
|
-
return [action, filters.filter(isDynamicQueryFilter)];
|
|
810
|
-
}));
|
|
811
|
-
const tableSchema = tableHelper({
|
|
812
|
-
db,
|
|
813
|
-
table: tableName
|
|
814
|
-
});
|
|
815
|
-
if (Object.keys(tableSchema.primaryKey).length === 0) throw new RumbleError(`No primary key found for entity ${String(tableName)}`);
|
|
816
|
-
const primaryKeyField = Object.values(tableSchema.primaryKey)[0];
|
|
817
|
-
const distinctValues = createDistinctValuesFromSQLType(primaryKeyField.getSQLType());
|
|
818
|
-
const blockEverythingFilter = { where: { AND: [{ [primaryKeyField.name]: distinctValues.value1 }, { [primaryKeyField.name]: distinctValues.value2 }] } };
|
|
819
|
-
/**
|
|
820
|
-
* Packs the filters into a response object that can be applied for queries by the user
|
|
821
|
-
*/
|
|
822
|
-
function transformToResponse(queryFilters) {
|
|
823
|
-
const internalTransformer = (filters, mergedLimit) => {
|
|
824
|
-
const limit = lazy(() => {
|
|
825
|
-
if (mergedLimit !== void 0) {
|
|
826
|
-
if (!filters?.limit) return mergedLimit;
|
|
827
|
-
if (filters.limit > mergedLimit) return mergedLimit;
|
|
828
|
-
}
|
|
829
|
-
let limit = filters?.limit;
|
|
830
|
-
if (defaultLimit && (limit === void 0 || limit > defaultLimit)) limit = defaultLimit;
|
|
831
|
-
return limit ?? void 0;
|
|
832
|
-
});
|
|
833
|
-
const sqlTransformedWhere = lazy(() => {
|
|
834
|
-
const casing = db._.session?.dialect?.casing;
|
|
835
|
-
return filters?.where ? relationsFilterToSQL(tableSchema.foundRelation.table, sanitizeFilterValue(filters.where), tableSchema.relations, db._.relations, casing) : void 0;
|
|
836
|
-
});
|
|
837
|
-
if (filters?.columns) return {
|
|
838
|
-
/**
|
|
839
|
-
* Query filters for the drizzle query API.
|
|
840
|
-
* @example
|
|
841
|
-
* ```ts
|
|
842
|
-
* author: t.relation("author", {
|
|
843
|
-
* query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
|
|
844
|
-
* }),
|
|
845
|
-
* ´´´
|
|
846
|
-
*/
|
|
847
|
-
query: {
|
|
848
|
-
/**
|
|
849
|
-
* For find first calls
|
|
850
|
-
*/
|
|
851
|
-
single: {
|
|
852
|
-
extras: filters?.extras,
|
|
853
|
-
where: sanitizeFilterValue(filters?.where),
|
|
854
|
-
columns: filters?.columns
|
|
855
|
-
},
|
|
856
|
-
/**
|
|
857
|
-
* For find many calls
|
|
858
|
-
*/
|
|
859
|
-
many: {
|
|
860
|
-
extras: filters?.extras,
|
|
861
|
-
where: sanitizeFilterValue(filters?.where),
|
|
862
|
-
columns: filters?.columns,
|
|
863
|
-
get limit() {
|
|
864
|
-
return limit();
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
},
|
|
868
|
-
/**
|
|
869
|
-
* Query filters for the drizzle SQL API as used in e.g. updates.
|
|
870
|
-
* @example
|
|
871
|
-
*
|
|
872
|
-
* ```ts
|
|
873
|
-
* await db
|
|
874
|
-
* .update(schema.users)
|
|
875
|
-
* .set({
|
|
876
|
-
* name: args.newName,
|
|
877
|
-
* })
|
|
878
|
-
* .where(
|
|
879
|
-
* and(
|
|
880
|
-
* eq(schema.users.id, args.userId),
|
|
881
|
-
* (await ctx.abilities.users.filter("update")).sql.where,
|
|
882
|
-
* ),
|
|
883
|
-
* );
|
|
884
|
-
* ```
|
|
885
|
-
*
|
|
886
|
-
*/
|
|
887
|
-
sql: { get where() {
|
|
888
|
-
return sqlTransformedWhere();
|
|
889
|
-
} }
|
|
890
|
-
};
|
|
891
|
-
else return {
|
|
892
|
-
/**
|
|
893
|
-
* Query filters for the drizzle query API.
|
|
894
|
-
* @example
|
|
895
|
-
* ```ts
|
|
896
|
-
* author: t.relation("author", {
|
|
897
|
-
* query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
|
|
898
|
-
* }),
|
|
899
|
-
* ´´´
|
|
900
|
-
*/
|
|
901
|
-
query: {
|
|
902
|
-
/**
|
|
903
|
-
* For find first calls
|
|
904
|
-
*/
|
|
905
|
-
single: {
|
|
906
|
-
extras: filters?.extras,
|
|
907
|
-
where: sanitizeFilterValue(filters?.where)
|
|
908
|
-
},
|
|
909
|
-
/**
|
|
910
|
-
* For find many calls
|
|
911
|
-
*/
|
|
912
|
-
many: {
|
|
913
|
-
extras: filters?.extras,
|
|
914
|
-
where: sanitizeFilterValue(filters?.where),
|
|
915
|
-
get limit() {
|
|
916
|
-
return limit();
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
},
|
|
920
|
-
/**
|
|
921
|
-
* Query filters for the drizzle SQL API as used in e.g. updates.
|
|
922
|
-
* @example
|
|
923
|
-
*
|
|
924
|
-
* ```ts
|
|
925
|
-
* await db
|
|
926
|
-
* .update(schema.users)
|
|
927
|
-
* .set({
|
|
928
|
-
* name: args.newName,
|
|
929
|
-
* })
|
|
930
|
-
* .where(
|
|
931
|
-
* and(
|
|
932
|
-
* eq(schema.users.id, args.userId),
|
|
933
|
-
* (await ctx.abilities.users.filter("update")).sql.where,
|
|
934
|
-
* ),
|
|
935
|
-
* );
|
|
936
|
-
* ```
|
|
937
|
-
*
|
|
938
|
-
*/
|
|
939
|
-
sql: { get where() {
|
|
940
|
-
return sqlTransformedWhere();
|
|
941
|
-
} }
|
|
942
|
-
};
|
|
943
|
-
};
|
|
944
|
-
const ret = internalTransformer(queryFilters);
|
|
945
|
-
/**
|
|
946
|
-
* Merges the current query filters with the provided filters for this call only
|
|
947
|
-
*/
|
|
948
|
-
function merge(p) {
|
|
949
|
-
const merged = mergeFilters(ret.query.many, p);
|
|
950
|
-
return internalTransformer(merged, p.limit);
|
|
951
|
-
}
|
|
952
|
-
ret.merge = merge;
|
|
953
|
-
return ret;
|
|
954
|
-
}
|
|
955
|
-
return { withContext: (userContext) => {
|
|
956
|
-
const prepare = (action) => {
|
|
957
|
-
const assembleAbilities = async (attributes) => {
|
|
958
|
-
const filters = queryFilters.get(action);
|
|
959
|
-
if (filters === "unrestricted") {
|
|
960
|
-
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
961
|
-
return transformToResponse();
|
|
962
|
-
}
|
|
963
|
-
if (!filters) {
|
|
964
|
-
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
965
|
-
nothingRegisteredWarningLogger(String(tableName), action);
|
|
966
|
-
return transformToResponse(blockEverythingFilter);
|
|
967
|
-
}
|
|
968
|
-
const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
|
|
969
|
-
if (rawResults.includes("allow")) {
|
|
970
|
-
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
971
|
-
return transformToResponse();
|
|
972
|
-
}
|
|
973
|
-
const dynamicResults = rawResults.filter((r) => r !== void 0);
|
|
974
|
-
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
975
|
-
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
976
|
-
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
977
|
-
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
978
|
-
if (allQueryFilters.length === 0) {
|
|
979
|
-
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
980
|
-
return transformToResponse(blockEverythingFilter);
|
|
981
|
-
}
|
|
982
|
-
const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
|
|
983
|
-
return mergeFilters(a, b, "OR");
|
|
984
|
-
});
|
|
985
|
-
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
986
|
-
return transformToResponse(mergedFilters);
|
|
987
|
-
};
|
|
988
|
-
const run = async (span) => {
|
|
989
|
-
const attributes = {
|
|
990
|
-
[ATTR_TABLE]: String(tableName),
|
|
991
|
-
[ATTR_ACTION]: action
|
|
992
|
-
};
|
|
993
|
-
try {
|
|
994
|
-
const result = await assembleAbilities(attributes);
|
|
995
|
-
log?.debug({
|
|
996
|
-
...attributes,
|
|
997
|
-
...traceCorrelationFields(telemetryConfig, span)
|
|
998
|
-
}, "abilities prepared");
|
|
999
|
-
return result;
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
recordSpanError(span, error);
|
|
1002
|
-
log?.error({
|
|
1003
|
-
...attributes,
|
|
1004
|
-
...traceCorrelationFields(telemetryConfig, span),
|
|
1005
|
-
...errorLogField(error)
|
|
1006
|
-
}, "abilities failed");
|
|
1007
|
-
throw error;
|
|
1008
|
-
} finally {
|
|
1009
|
-
span?.setAttributes(attributes);
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
|
|
1013
|
-
try {
|
|
1014
|
-
return await run(span);
|
|
1015
|
-
} finally {
|
|
1016
|
-
span.end();
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1019
|
-
return run();
|
|
1020
|
-
};
|
|
1021
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1022
|
-
return { filter: (action) => {
|
|
1023
|
-
let prepared = cache.get(action);
|
|
1024
|
-
if (!prepared) {
|
|
1025
|
-
prepared = guardAgainstMissingAwait(prepare(action));
|
|
1026
|
-
cache.set(action, prepared);
|
|
1027
|
-
}
|
|
1028
|
-
return prepared;
|
|
1029
|
-
} };
|
|
1030
|
-
} };
|
|
1031
|
-
};
|
|
1032
|
-
const abilitiesPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createFilterForTable(tableName)]));
|
|
1246
|
+
const abilitiesPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createTableAbilities(settings, tableName, buildersPerTable[tableName]._.queryFilters)]));
|
|
1033
1247
|
hasBeenBuilt = true;
|
|
1034
1248
|
return (ctx) => {
|
|
1035
|
-
|
|
1249
|
+
const state = createRequestState();
|
|
1250
|
+
const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
|
|
1251
|
+
attachRequestState(abilities, state);
|
|
1252
|
+
return abilities;
|
|
1036
1253
|
};
|
|
1037
1254
|
}
|
|
1038
1255
|
}
|
|
@@ -1856,7 +2073,8 @@ async function initSearchIfApplicable(input) {
|
|
|
1856
2073
|
*/
|
|
1857
2074
|
function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
1858
2075
|
if (search?.enabled && args.search && args.search.length > 0) {
|
|
1859
|
-
const
|
|
2076
|
+
const columnMask = abilities[columnMaskKey];
|
|
2077
|
+
const columnsToSearch = (columnMask ? Object.entries(tableSchema.columns).filter(([key]) => columnMask.guaranteed.has(key)) : Object.entries(tableSchema.columns)).filter(([_key, col]) => isStringLikeSQLTypeString(col.getSQLType()) || isIDLikeSQLTypeString(col.getSQLType()));
|
|
1860
2078
|
const searchParam = sql`${args.search}`;
|
|
1861
2079
|
args.extras = { search_distance: (table) => sql`${sql.join(columnsToSearch.map(([key]) => {
|
|
1862
2080
|
return sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
|
|
@@ -1914,14 +2132,15 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
1914
2132
|
db,
|
|
1915
2133
|
table
|
|
1916
2134
|
});
|
|
2135
|
+
abilityBuilder._.registerReadAction(table, readAction);
|
|
1917
2136
|
if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
|
|
1918
|
-
const
|
|
2137
|
+
const primaryKeyName = Object.keys(tableSchema.primaryKey)[0];
|
|
1919
2138
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
1920
2139
|
return schemaBuilder.drizzleObject(table, {
|
|
1921
2140
|
name: refName ?? capitalize(String(table)),
|
|
1922
2141
|
subscribe: (subscriptions, element, _context) => {
|
|
1923
|
-
if (!
|
|
1924
|
-
const primaryKeyValue = element[
|
|
2142
|
+
if (!primaryKeyName) return;
|
|
2143
|
+
const primaryKeyValue = element[primaryKeyName];
|
|
1925
2144
|
if (!primaryKeyValue) {
|
|
1926
2145
|
warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
|
|
1927
2146
|
return;
|
|
@@ -1936,6 +2155,16 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
1936
2155
|
table,
|
|
1937
2156
|
action: readAction
|
|
1938
2157
|
}),
|
|
2158
|
+
maskColumns: ({ context, entities, action }) => abilityBuilder._.maskColumns({
|
|
2159
|
+
table,
|
|
2160
|
+
action: action ?? readAction,
|
|
2161
|
+
abilities: context.abilities,
|
|
2162
|
+
entities
|
|
2163
|
+
}),
|
|
2164
|
+
maskPrimaryKeys: {
|
|
2165
|
+
fields: Object.keys(tableSchema.primaryKey),
|
|
2166
|
+
isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
|
|
2167
|
+
},
|
|
1939
2168
|
fields: (t) => {
|
|
1940
2169
|
const columns = tableSchema.columns;
|
|
1941
2170
|
const configMap = /* @__PURE__ */ new Map();
|
|
@@ -2171,7 +2400,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2171
2400
|
db,
|
|
2172
2401
|
table
|
|
2173
2402
|
});
|
|
2174
|
-
const
|
|
2403
|
+
const primaryKeyName = Object.keys(tableSchema.primaryKey)[0];
|
|
2175
2404
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
2176
2405
|
const pluralName = pluralize.plural(String(table));
|
|
2177
2406
|
const singularName = pluralize.singular(String(table));
|
|
@@ -2199,6 +2428,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2199
2428
|
type: [table],
|
|
2200
2429
|
nullable: false,
|
|
2201
2430
|
smartSubscription: true,
|
|
2431
|
+
maskColumnsAction: listAction,
|
|
2202
2432
|
description: `List all ${pluralName}`,
|
|
2203
2433
|
subscribe: (subscriptions, _root, _args, _ctx, _info) => {
|
|
2204
2434
|
registerOnInstance({
|
|
@@ -2246,11 +2476,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2246
2476
|
type: table,
|
|
2247
2477
|
nullable: false,
|
|
2248
2478
|
smartSubscription: true,
|
|
2479
|
+
maskColumnsAction: readAction,
|
|
2249
2480
|
description: `Get a single ${singularName} by ID`,
|
|
2250
2481
|
args: { id: t.arg.id({ required: true }) },
|
|
2251
2482
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
2252
2483
|
deepSetProto(args);
|
|
2253
|
-
const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [
|
|
2484
|
+
const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [primaryKeyName]: args.id } }).query.single;
|
|
2254
2485
|
const queryArgs = query(filter);
|
|
2255
2486
|
if (filter.columns) queryArgs.columns = filter.columns;
|
|
2256
2487
|
return findFirst ? findFirst({
|
|
@@ -2518,6 +2749,108 @@ function implementDefaultWhereInputArgs(schemaBuilder) {
|
|
|
2518
2749
|
}) });
|
|
2519
2750
|
}
|
|
2520
2751
|
//#endregion
|
|
2752
|
+
//#region lib/helpers/objectTypeOptions.ts
|
|
2753
|
+
function objectTypeOptionsOfField(buildCache, fieldConfig) {
|
|
2754
|
+
const type = fieldConfig.type;
|
|
2755
|
+
const ref = type.kind === "List" ? type.type.kind === "Object" ? type.type.ref : void 0 : type.kind === "Object" ? type.ref : void 0;
|
|
2756
|
+
if (!ref) return void 0;
|
|
2757
|
+
return buildCache.getTypeConfig(ref, "Object").pothosOptions;
|
|
2758
|
+
}
|
|
2759
|
+
//#endregion
|
|
2760
|
+
//#region lib/helpers/registerPlugin.ts
|
|
2761
|
+
const registered = /* @__PURE__ */ new Set();
|
|
2762
|
+
function registerPluginOnce(name, plugin) {
|
|
2763
|
+
if (registered.has(name)) return;
|
|
2764
|
+
SchemaBuilder.allowPluginReRegistration = true;
|
|
2765
|
+
SchemaBuilder.registerPlugin(name, plugin);
|
|
2766
|
+
registered.add(name);
|
|
2767
|
+
}
|
|
2768
|
+
//#endregion
|
|
2769
|
+
//#region lib/columnMaskPlugin/maskTypes.ts
|
|
2770
|
+
const maskColumnsKey = "maskColumns";
|
|
2771
|
+
const maskColumnsActionKey = "maskColumnsAction";
|
|
2772
|
+
const maskPrimaryKeysKey = "maskPrimaryKeys";
|
|
2773
|
+
const columnMaskPluginName = "ColumnMaskPlugin";
|
|
2774
|
+
//#endregion
|
|
2775
|
+
//#region lib/columnMaskPlugin/columnMaskPlugin.ts
|
|
2776
|
+
var ColumnMaskPlugin = class extends BasePlugin {
|
|
2777
|
+
wrapResolve(resolver, fieldConfig) {
|
|
2778
|
+
return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
|
|
2779
|
+
}
|
|
2780
|
+
maskPrimaryKey(resolver, fieldConfig) {
|
|
2781
|
+
const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
|
|
2782
|
+
if (!primaryKeys?.fields.includes(fieldConfig.name)) return resolver;
|
|
2783
|
+
return (parent, args, context, info) => primaryKeys.isHidden(context, parent) ? null : resolver(parent, args, context, info);
|
|
2784
|
+
}
|
|
2785
|
+
maskResult(resolver, fieldConfig) {
|
|
2786
|
+
const maskColumns = objectTypeOptionsOfField(this.buildCache, fieldConfig)?.[maskColumnsKey];
|
|
2787
|
+
if (!maskColumns) return resolver;
|
|
2788
|
+
const action = fieldConfig.pothosOptions[maskColumnsActionKey];
|
|
2789
|
+
const mask = (value, context) => {
|
|
2790
|
+
if (value == null) return value;
|
|
2791
|
+
if (Array.isArray(value) && value.some(isThenable)) return Promise.all(value).then((rows) => mask(rows, context));
|
|
2792
|
+
const pending = maskColumns({
|
|
2793
|
+
context,
|
|
2794
|
+
entities: Array.isArray(value) ? value : [value],
|
|
2795
|
+
action
|
|
2796
|
+
});
|
|
2797
|
+
return pending ? pending.then(() => value) : value;
|
|
2798
|
+
};
|
|
2799
|
+
return (parent, args, context, info) => {
|
|
2800
|
+
const resolved = resolver(parent, args, context, info);
|
|
2801
|
+
return isThenable(resolved) ? resolved.then((value) => mask(value, context)) : mask(resolved, context);
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
function registerColumnMaskPlugin() {
|
|
2806
|
+
registerPluginOnce(columnMaskPluginName, ColumnMaskPlugin);
|
|
2807
|
+
}
|
|
2808
|
+
//#endregion
|
|
2809
|
+
//#region lib/helpers/abilityAwareClient.ts
|
|
2810
|
+
function createAbilityAwareClient({ db, abilityBuilder }) {
|
|
2811
|
+
return (context) => {
|
|
2812
|
+
const apis = /* @__PURE__ */ new Map();
|
|
2813
|
+
const query = new Proxy(db.query, { get(target, table, receiver) {
|
|
2814
|
+
const api = Reflect.get(target, table, receiver);
|
|
2815
|
+
if (typeof table !== "string" || !context.abilities?.[table]) return api;
|
|
2816
|
+
let wrapped = apis.get(table);
|
|
2817
|
+
if (!wrapped) {
|
|
2818
|
+
wrapped = Object.create(api, { findMany: { value: async (config = {}) => {
|
|
2819
|
+
const masked = abilityBuilder._.maskedActions(context.abilities, table);
|
|
2820
|
+
const actions = masked.size ? [...masked] : [abilityBuilder._.readActionOf(table)];
|
|
2821
|
+
return (await Promise.all(actions.map(async (action) => {
|
|
2822
|
+
const ability = (await context.abilities[table].filter(action)).query.many;
|
|
2823
|
+
const { where, extras } = mergeFilters({
|
|
2824
|
+
where: config.where,
|
|
2825
|
+
extras: config.extras
|
|
2826
|
+
}, {
|
|
2827
|
+
where: ability.where,
|
|
2828
|
+
extras: ability.extras
|
|
2829
|
+
});
|
|
2830
|
+
const rows = await api.findMany({
|
|
2831
|
+
...config,
|
|
2832
|
+
where,
|
|
2833
|
+
extras
|
|
2834
|
+
});
|
|
2835
|
+
await abilityBuilder._.maskColumns({
|
|
2836
|
+
table,
|
|
2837
|
+
action,
|
|
2838
|
+
abilities: context.abilities,
|
|
2839
|
+
entities: rows
|
|
2840
|
+
});
|
|
2841
|
+
return rows;
|
|
2842
|
+
}))).flat();
|
|
2843
|
+
} } });
|
|
2844
|
+
apis.set(table, wrapped);
|
|
2845
|
+
}
|
|
2846
|
+
return wrapped;
|
|
2847
|
+
} });
|
|
2848
|
+
return new Proxy(db, { get(target, prop, receiver) {
|
|
2849
|
+
return prop === "query" ? query : Reflect.get(target, prop, receiver);
|
|
2850
|
+
} });
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
//#endregion
|
|
2521
2854
|
//#region lib/runtimeFiltersPlugin/filterTypes.ts
|
|
2522
2855
|
const pluginName = "RuntimeFiltersPlugin";
|
|
2523
2856
|
//#endregion
|
|
@@ -2528,14 +2861,9 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2528
2861
|
tracerEnabled;
|
|
2529
2862
|
logger;
|
|
2530
2863
|
telemetryConfig = {};
|
|
2531
|
-
filterLoaders = /* @__PURE__ */ new WeakMap();
|
|
2532
2864
|
getLoader(context, filter) {
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
perContext = /* @__PURE__ */ new Map();
|
|
2536
|
-
this.filterLoaders.set(context, perContext);
|
|
2537
|
-
}
|
|
2538
|
-
let loader = perContext.get(filter);
|
|
2865
|
+
const loaders = requestStateOf(context.abilities)?.filterLoaders ?? /* @__PURE__ */ new Map();
|
|
2866
|
+
let loader = loaders.get(filter);
|
|
2539
2867
|
if (!loader) {
|
|
2540
2868
|
const prefetchPromise = filter.prefetch ? filter.prefetch({ context }) : void 0;
|
|
2541
2869
|
loader = new DataLoader(async (entities) => {
|
|
@@ -2548,7 +2876,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2548
2876
|
const allowedSet = new Set(allowed);
|
|
2549
2877
|
return entities.map((entity) => allowedSet.has(entity) ? entity : null);
|
|
2550
2878
|
}, { cache: false });
|
|
2551
|
-
|
|
2879
|
+
loaders.set(filter, loader);
|
|
2552
2880
|
}
|
|
2553
2881
|
return loader;
|
|
2554
2882
|
}
|
|
@@ -2563,14 +2891,11 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2563
2891
|
return typeConfig;
|
|
2564
2892
|
}
|
|
2565
2893
|
wrapResolve(resolver, fieldConfig) {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
else if (fieldType.kind === "Object") filters = fieldType.ref.currentConfig.pothosOptions[applyFiltersKey];
|
|
2571
|
-
if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
|
|
2894
|
+
const filters = objectTypeOptionsOfField(this.buildCache, fieldConfig)?.[applyFiltersKey];
|
|
2895
|
+
if (!filters) return resolver;
|
|
2896
|
+
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2897
|
+
const applyFilters = async (parent, args, context, info) => {
|
|
2572
2898
|
const runFilters = async (span) => {
|
|
2573
|
-
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2574
2899
|
span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
|
|
2575
2900
|
const loaders = allFilters.map((filter) => this.getLoader(context, filter));
|
|
2576
2901
|
let resolved;
|
|
@@ -2625,15 +2950,11 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2625
2950
|
throw error;
|
|
2626
2951
|
}
|
|
2627
2952
|
};
|
|
2953
|
+
return (parent, args, context, info) => allFilters.length === 0 ? resolver(parent, args, context, info) : applyFilters(parent, args, context, info);
|
|
2628
2954
|
}
|
|
2629
2955
|
};
|
|
2630
|
-
let registered = false;
|
|
2631
2956
|
function registerRuntimeFiltersPlugin() {
|
|
2632
|
-
|
|
2633
|
-
SchemaBuilder.allowPluginReRegistration = true;
|
|
2634
|
-
SchemaBuilder.registerPlugin(pluginName, RuntimeFiltersPlugin);
|
|
2635
|
-
registered = true;
|
|
2636
|
-
}
|
|
2957
|
+
registerPluginOnce(pluginName, RuntimeFiltersPlugin);
|
|
2637
2958
|
}
|
|
2638
2959
|
//#endregion
|
|
2639
2960
|
//#region lib/scalars/addressValidation.ts
|
|
@@ -2764,9 +3085,13 @@ function implementDefaultAddressTypes(schemaBuilder) {
|
|
|
2764
3085
|
//#endregion
|
|
2765
3086
|
//#region lib/scalars/emailAddress.ts
|
|
2766
3087
|
/**
|
|
2767
|
-
* Validates an email address using graphql-scalars' own format check, then lowercases it
|
|
3088
|
+
* Validates an email address using graphql-scalars' own format check, then lowercases it, exactly
|
|
3089
|
+
* as the `EmailAddress` scalar does. Exported for values that do not arrive as GraphQL input,
|
|
3090
|
+
* such as OIDC claims.
|
|
3091
|
+
*
|
|
3092
|
+
* @throws RumbleErrorSafe if the value is not a valid email address
|
|
2768
3093
|
*/
|
|
2769
|
-
const
|
|
3094
|
+
const normalizeEmailAddress = (value) => {
|
|
2770
3095
|
try {
|
|
2771
3096
|
return EmailAddressResolver.parseValue(value).toLowerCase();
|
|
2772
3097
|
} catch (error) {
|
|
@@ -2777,23 +3102,28 @@ const EmailAddressResolver$1 = new GraphQLScalarType({
|
|
|
2777
3102
|
name: "EmailAddress",
|
|
2778
3103
|
description: `${EmailAddressResolver.description} Lowercased.`,
|
|
2779
3104
|
specifiedByURL: EmailAddressResolver.specifiedByURL,
|
|
2780
|
-
serialize:
|
|
2781
|
-
parseValue:
|
|
3105
|
+
serialize: normalizeEmailAddress,
|
|
3106
|
+
parseValue: normalizeEmailAddress,
|
|
2782
3107
|
parseLiteral(ast) {
|
|
2783
3108
|
if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as email addresses but got a: ${ast.kind}`);
|
|
2784
|
-
return
|
|
3109
|
+
return normalizeEmailAddress(ast.value);
|
|
2785
3110
|
}
|
|
2786
3111
|
});
|
|
2787
3112
|
//#endregion
|
|
2788
3113
|
//#region lib/scalars/personName.ts
|
|
2789
3114
|
const MAX_LENGTH = 200;
|
|
2790
3115
|
const NAME_REGEX = /^[\p{L}\p{M}]+(?:(?:[ '-]|\.\x20?)[\p{L}\p{M}]+)*\.?$/u;
|
|
3116
|
+
const APOSTROPHE_VARIANTS = /[\u2018\u2019\u02BC\u00B4\u0060]/gu;
|
|
3117
|
+
const HYPHEN_VARIANTS = /[\u2010-\u2015\u2212]/gu;
|
|
2791
3118
|
/**
|
|
2792
|
-
* Normalizes and validates a person's name.
|
|
3119
|
+
* Normalizes and validates a person's name, exactly as the `PersonName` scalar does. Exported
|
|
3120
|
+
* for values that do not arrive as GraphQL input, such as OIDC claims.
|
|
3121
|
+
*
|
|
3122
|
+
* @throws RumbleErrorSafe if the value is not a valid name
|
|
2793
3123
|
*/
|
|
2794
|
-
const
|
|
3124
|
+
const normalizePersonName = (value) => {
|
|
2795
3125
|
if (typeof value !== "string") throw new RumbleErrorSafe(`Name value is not a string: ${value}`);
|
|
2796
|
-
const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\p{Zs}+/gu, " ").trim().replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
|
|
3126
|
+
const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(APOSTROPHE_VARIANTS, "'").replace(HYPHEN_VARIANTS, "-").replace(/\p{Zs}+/gu, " ").trim().replace(/ ?- ?/gu, "-").replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
|
|
2797
3127
|
if (normalized.length === 0) throw new RumbleErrorSafe("Name must not be empty.");
|
|
2798
3128
|
if (normalized.length > MAX_LENGTH) throw new RumbleErrorSafe(`Name must not be longer than ${MAX_LENGTH} characters.`);
|
|
2799
3129
|
if (!NAME_REGEX.test(normalized)) throw new RumbleErrorSafe(`Invalid name: ${value}. Names may only contain letters and the separators space, apostrophe, hyphen and period, and may not start with or repeat a separator.`);
|
|
@@ -2801,12 +3131,12 @@ const normalize = (value) => {
|
|
|
2801
3131
|
};
|
|
2802
3132
|
const PersonNameResolver = new GraphQLScalarType({
|
|
2803
3133
|
name: "PersonName",
|
|
2804
|
-
description: "A person's name. Unicode letters from any script are allowed, along with space, apostrophe, hyphen and period as separators between name parts. The value is Unicode NFC normalized, has invisible/control characters stripped, has its whitespace collapsed, and has unambiguous typos (a doubled or stray leading/trailing hyphen/apostrophe) silently corrected rather than rejected.",
|
|
2805
|
-
serialize:
|
|
2806
|
-
parseValue:
|
|
3134
|
+
description: "A person's name. Unicode letters from any script are allowed, along with space, apostrophe, hyphen and period as separators between name parts. The value is Unicode NFC normalized, has invisible/control characters stripped, has its whitespace collapsed, and has unambiguous typos (typographic apostrophe or dash variants, spaces around a hyphen, a doubled or stray leading/trailing hyphen/apostrophe) silently corrected rather than rejected.",
|
|
3135
|
+
serialize: normalizePersonName,
|
|
3136
|
+
parseValue: normalizePersonName,
|
|
2807
3137
|
parseLiteral(ast) {
|
|
2808
3138
|
if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as names but got a: ${ast.kind}`);
|
|
2809
|
-
return
|
|
3139
|
+
return normalizePersonName(ast.value);
|
|
2810
3140
|
}
|
|
2811
3141
|
});
|
|
2812
3142
|
//#endregion
|
|
@@ -2814,9 +3144,12 @@ const PersonNameResolver = new GraphQLScalarType({
|
|
|
2814
3144
|
/**
|
|
2815
3145
|
* Parses and validates a phone number using libphonenumber-js (Google's phone number metadata),
|
|
2816
3146
|
* which checks real per-country validity (length, area code plausibility, ...) rather than just
|
|
2817
|
-
* the E.164 shape.
|
|
3147
|
+
* the E.164 shape. This is what the `PhoneNumber` scalar does with its input; exported for values
|
|
3148
|
+
* that do not arrive as GraphQL input. `.number` of the result is the E.164 form.
|
|
3149
|
+
*
|
|
3150
|
+
* @throws RumbleErrorSafe if the value is not a valid phone number
|
|
2818
3151
|
*/
|
|
2819
|
-
const
|
|
3152
|
+
const parsePhoneNumber = (value) => {
|
|
2820
3153
|
if (typeof value !== "string") throw new RumbleErrorSafe(`Phone number value is not a string: ${value}`);
|
|
2821
3154
|
const phoneNumber = parsePhoneNumberFromString(value);
|
|
2822
3155
|
if (!phoneNumber?.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${value}. Please provide a valid phone number including its country calling code, e.g. +4915123456789.`);
|
|
@@ -2832,26 +3165,28 @@ const serialize = (value) => {
|
|
|
2832
3165
|
if (!phoneNumber.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${phoneNumber.number}`);
|
|
2833
3166
|
return phoneNumber.number;
|
|
2834
3167
|
}
|
|
2835
|
-
return
|
|
3168
|
+
return parsePhoneNumber(value).number;
|
|
2836
3169
|
};
|
|
2837
3170
|
const PhoneNumberResolver = new GraphQLScalarType({
|
|
2838
3171
|
name: "PhoneNumber",
|
|
2839
3172
|
description: "A phone number, validated using real per-country validity rules and represented as a parsed libphonenumber-js PhoneNumber. On the wire it is transported as the E.164 format (https://en.wikipedia.org/wiki/E.164), e.g. +4915123456789. The country calling code is required.",
|
|
2840
3173
|
serialize,
|
|
2841
|
-
parseValue:
|
|
3174
|
+
parseValue: parsePhoneNumber,
|
|
2842
3175
|
parseLiteral(ast) {
|
|
2843
3176
|
if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as phone numbers but got a: ${ast.kind}`);
|
|
2844
|
-
return
|
|
3177
|
+
return parsePhoneNumber(ast.value);
|
|
2845
3178
|
}
|
|
2846
3179
|
});
|
|
2847
3180
|
//#endregion
|
|
2848
3181
|
//#region lib/schemaBuilder.ts
|
|
2849
|
-
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
|
|
3182
|
+
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation, abilityBuilder }) => {
|
|
2850
3183
|
const createSpan = otel?.enabled && otel.tracer ? createOpenTelemetryWrapper(otel.tracer, otel.options) : void 0;
|
|
2851
3184
|
registerRuntimeFiltersPlugin();
|
|
3185
|
+
registerColumnMaskPlugin();
|
|
2852
3186
|
const schemaBuilder = new SchemaBuilder({
|
|
2853
3187
|
...pothosConfig,
|
|
2854
3188
|
plugins: [
|
|
3189
|
+
columnMaskPluginName,
|
|
2855
3190
|
pluginName,
|
|
2856
3191
|
DrizzlePlugin,
|
|
2857
3192
|
SmartSubscriptionsPlugin,
|
|
@@ -2860,13 +3195,19 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validatio
|
|
|
2860
3195
|
...pothosConfig?.plugins ?? []
|
|
2861
3196
|
],
|
|
2862
3197
|
drizzle: {
|
|
2863
|
-
client:
|
|
3198
|
+
client: createAbilityAwareClient({
|
|
3199
|
+
db,
|
|
3200
|
+
abilityBuilder
|
|
3201
|
+
}),
|
|
2864
3202
|
relations: db._.relations,
|
|
2865
3203
|
getTableConfig(table) {
|
|
2866
|
-
|
|
3204
|
+
if (is(table, PgTable)) return getTableConfig(table);
|
|
3205
|
+
if (is(table, MySqlTable)) return getTableConfig$1(table);
|
|
3206
|
+
if (is(table, SQLiteTable)) return getTableConfig$2(table);
|
|
2867
3207
|
return {
|
|
2868
|
-
columns,
|
|
2869
|
-
primaryKeys:
|
|
3208
|
+
columns: isTable(table) ? Object.values(getTableColumns(table)) : [],
|
|
3209
|
+
primaryKeys: [],
|
|
3210
|
+
uniqueConstraints: []
|
|
2870
3211
|
};
|
|
2871
3212
|
}
|
|
2872
3213
|
},
|
|
@@ -2985,7 +3326,8 @@ export const r = rumble({
|
|
|
2985
3326
|
const { makePubSubInstance, pubsub } = createPubSubInstance({ ...rumbleInput });
|
|
2986
3327
|
const { schemaBuilder } = createSchemaBuilder({
|
|
2987
3328
|
...rumbleInput,
|
|
2988
|
-
pubsub
|
|
3329
|
+
pubsub,
|
|
3330
|
+
abilityBuilder
|
|
2989
3331
|
});
|
|
2990
3332
|
const enum_ = createEnumImplementer({
|
|
2991
3333
|
...rumbleInput,
|
|
@@ -3249,6 +3591,6 @@ export const r = rumble({
|
|
|
3249
3591
|
};
|
|
3250
3592
|
};
|
|
3251
3593
|
//#endregion
|
|
3252
|
-
export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, mapNullFieldsToUndefined, rumble };
|
|
3594
|
+
export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, mapNullFieldsToUndefined, normalizeEmailAddress, normalizePersonName, parsePhoneNumber, rumble };
|
|
3253
3595
|
|
|
3254
3596
|
//# sourceMappingURL=index.mjs.map
|