@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.cjs
CHANGED
|
@@ -216,38 +216,76 @@ function mapNullFieldsToUndefined(obj) {
|
|
|
216
216
|
}
|
|
217
217
|
//#endregion
|
|
218
218
|
//#region package.json
|
|
219
|
-
var version = "0.
|
|
219
|
+
var version = "0.25.2";
|
|
220
220
|
//#endregion
|
|
221
|
-
//#region lib/helpers/
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
221
|
+
//#region lib/helpers/tableHelpers.ts
|
|
222
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
223
|
+
function buildIndex(db) {
|
|
224
|
+
const byTsName = /* @__PURE__ */ new Map();
|
|
225
|
+
const byDbName = /* @__PURE__ */ new Map();
|
|
226
|
+
const byTableRef = /* @__PURE__ */ new Map();
|
|
227
|
+
for (const [tsName, rawEntry] of Object.entries(db._.relations ?? {})) {
|
|
228
|
+
const entry = rawEntry;
|
|
229
|
+
const tableObj = entry.table;
|
|
230
|
+
if (!tableObj) continue;
|
|
231
|
+
const columns = (0, drizzle_orm.isTable)(tableObj) ? (0, drizzle_orm.getTableColumns)(tableObj) : {};
|
|
232
|
+
const primaryKey = Object.fromEntries(Object.entries(columns).filter(([, col]) => col.primary));
|
|
233
|
+
const dbName = (0, drizzle_orm.isTable)(tableObj) ? (0, drizzle_orm.getTableName)(tableObj) : entry.name ?? tsName;
|
|
234
|
+
const resolved = {
|
|
235
|
+
name: entry.name ?? tsName,
|
|
236
|
+
dbName,
|
|
237
|
+
table: tableObj,
|
|
238
|
+
columns,
|
|
239
|
+
primaryKey,
|
|
240
|
+
relations: entry.relations ?? {},
|
|
241
|
+
isTable: (0, drizzle_orm.isTable)(tableObj),
|
|
242
|
+
foundRelation: entry
|
|
234
243
|
};
|
|
244
|
+
byTsName.set(resolved.name, resolved);
|
|
245
|
+
byDbName.set(resolved.dbName, resolved);
|
|
246
|
+
byTableRef.set(tableObj, resolved);
|
|
235
247
|
}
|
|
236
248
|
return {
|
|
237
|
-
|
|
238
|
-
|
|
249
|
+
byTsName,
|
|
250
|
+
byDbName,
|
|
251
|
+
byTableRef
|
|
239
252
|
};
|
|
240
253
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return
|
|
254
|
+
function getIndex(db) {
|
|
255
|
+
let idx = indexCache.get(db);
|
|
256
|
+
if (!idx) {
|
|
257
|
+
idx = buildIndex(db);
|
|
258
|
+
indexCache.set(db, idx);
|
|
259
|
+
}
|
|
260
|
+
return idx;
|
|
248
261
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
262
|
+
function tableHelper({ db, table }) {
|
|
263
|
+
const idx = getIndex(db);
|
|
264
|
+
let entry;
|
|
265
|
+
if (typeof table === "string") entry = idx.byTsName.get(table) ?? idx.byDbName.get(table);
|
|
266
|
+
else if (table && typeof table === "object") {
|
|
267
|
+
const t = table;
|
|
268
|
+
if (t.table) {
|
|
269
|
+
entry = idx.byTableRef.get(t.table);
|
|
270
|
+
if (!entry && t.name) entry = idx.byTsName.get(t.name) ?? idx.byDbName.get(t.name);
|
|
271
|
+
}
|
|
272
|
+
if (!entry) entry = idx.byTableRef.get(t);
|
|
273
|
+
if (!entry && (0, drizzle_orm.isTable)(t)) {
|
|
274
|
+
const dbName = (0, drizzle_orm.getTableName)(t);
|
|
275
|
+
entry = idx.byDbName.get(dbName);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!entry) throw new RumbleError(`Could not find schema for ${JSON.stringify(typeof table === "string" ? table : "<table-object>")}`);
|
|
279
|
+
return {
|
|
280
|
+
columns: entry.columns,
|
|
281
|
+
primaryKey: entry.primaryKey,
|
|
282
|
+
relations: entry.relations,
|
|
283
|
+
dbName: entry.dbName,
|
|
284
|
+
tsName: entry.name,
|
|
285
|
+
table: entry.table,
|
|
286
|
+
isTable: entry.isTable,
|
|
287
|
+
foundRelation: entry.foundRelation
|
|
288
|
+
};
|
|
251
289
|
}
|
|
252
290
|
//#endregion
|
|
253
291
|
//#region lib/helpers/mergeFilters.ts
|
|
@@ -262,20 +300,36 @@ const EmptyFilter$1 = Symbol.for("drizzle:EmptyFilter");
|
|
|
262
300
|
function realWhere(where) {
|
|
263
301
|
return where === EmptyFilter$1 ? void 0 : where;
|
|
264
302
|
}
|
|
303
|
+
function mergeWhere(a, b, mode) {
|
|
304
|
+
if (a && b) return mode === "OR" ? { OR: [a, b] } : { AND: [a, b] };
|
|
305
|
+
return mode === "OR" ? void 0 : a ?? b;
|
|
306
|
+
}
|
|
307
|
+
function mergeColumns(a, b) {
|
|
308
|
+
if (!a && !b) return void 0;
|
|
309
|
+
const result = {};
|
|
310
|
+
for (const [key, value] of [...Object.entries(a ?? {}), ...Object.entries(b ?? {})]) if (value === true) result[key] = true;
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
function mergeRecords(a, b) {
|
|
314
|
+
return a || b ? (0, es_toolkit.toMerged)(a ?? {}, b ?? {}) : void 0;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Combines two limit/offset style bounds. For OR the looser bound wins (and a
|
|
318
|
+
* missing side means unbounded), for AND the stricter bound wins.
|
|
319
|
+
*/
|
|
320
|
+
function mergeBound(a, b, mode, orPick) {
|
|
321
|
+
if (mode === "OR") return a === void 0 || b === void 0 ? void 0 : orPick(a, b);
|
|
322
|
+
return a || b ? Math.min(a ?? Infinity, b ?? Infinity) : void 0;
|
|
323
|
+
}
|
|
265
324
|
function mergeFilters(filterA, filterB, mode = "AND") {
|
|
266
|
-
const filterAWhere = realWhere(filterA?.where);
|
|
267
|
-
const filterBWhere = realWhere(filterB?.where);
|
|
268
325
|
return {
|
|
269
|
-
where:
|
|
270
|
-
columns:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
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,
|
|
277
|
-
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,
|
|
278
|
-
with: filterA?.with || filterB?.with ? (0, es_toolkit.toMerged)(filterA?.with ?? {}, filterB?.with ?? {}) : void 0
|
|
326
|
+
where: mergeWhere(realWhere(filterA?.where), realWhere(filterB?.where), mode),
|
|
327
|
+
columns: mergeColumns(filterA?.columns, filterB?.columns),
|
|
328
|
+
extras: mergeRecords(filterA?.extras, filterB?.extras),
|
|
329
|
+
orderBy: mergeRecords(filterA?.orderBy, filterB?.orderBy),
|
|
330
|
+
limit: mergeBound(filterA?.limit, filterB?.limit, mode, Math.max),
|
|
331
|
+
offset: mergeBound(filterA?.offset, filterB?.offset, mode, Math.min),
|
|
332
|
+
with: mergeRecords(filterA?.with, filterB?.with)
|
|
279
333
|
};
|
|
280
334
|
}
|
|
281
335
|
//#endregion
|
|
@@ -293,6 +347,201 @@ function sanitizeFilterValue(value) {
|
|
|
293
347
|
return value;
|
|
294
348
|
}
|
|
295
349
|
//#endregion
|
|
350
|
+
//#region lib/abilityBuilder/keys.ts
|
|
351
|
+
const ACTION_FLAG_KEY = "__rumble_action";
|
|
352
|
+
const columnFlagKey = (group) => `__rumble_columns_${group}`;
|
|
353
|
+
const columnMaskKey = Symbol.for("rumble:columnMask");
|
|
354
|
+
const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
|
|
355
|
+
const requestStateKey = Symbol.for("rumble:requestState");
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region lib/abilityBuilder/requestState.ts
|
|
358
|
+
function createRequestState() {
|
|
359
|
+
return {
|
|
360
|
+
masked: /* @__PURE__ */ new WeakMap(),
|
|
361
|
+
actions: /* @__PURE__ */ new Map(),
|
|
362
|
+
filterLoaders: /* @__PURE__ */ new Map()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function attachRequestState(abilities, state) {
|
|
366
|
+
Object.defineProperty(abilities, requestStateKey, { value: state });
|
|
367
|
+
}
|
|
368
|
+
function requestStateOf(abilities) {
|
|
369
|
+
return abilities?.[requestStateKey];
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region lib/abilityBuilder/columnMask.ts
|
|
373
|
+
function referencesRelation(filter, relations) {
|
|
374
|
+
if (!filter || typeof filter !== "object") return false;
|
|
375
|
+
return Object.entries(filter).some(([key, value]) => {
|
|
376
|
+
if (key === "AND" || key === "OR") return value.some((v) => referencesRelation(v, relations));
|
|
377
|
+
if (key === "NOT") return referencesRelation(value, relations);
|
|
378
|
+
return key in relations;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function selectedColumns(columns, allColumns) {
|
|
382
|
+
if (!columns) return new Set(allColumns);
|
|
383
|
+
const entries = Object.entries(columns).filter(([, v]) => v !== void 0);
|
|
384
|
+
if (entries.some(([, v]) => v)) return new Set(entries.filter(([, v]) => v).map(([k]) => k));
|
|
385
|
+
if (entries.length === 0) return /* @__PURE__ */ new Set();
|
|
386
|
+
return new Set(allColumns.filter((c) => columns[c] !== false));
|
|
387
|
+
}
|
|
388
|
+
function hiddenColumns(mask, matched, allColumns, primaryKey) {
|
|
389
|
+
let hidden = mask.hiddenPerMatch.get(matched);
|
|
390
|
+
if (!hidden) {
|
|
391
|
+
const columns = allColumns.filter((column) => !mask.guaranteed.has(column) && !mask.conditional.some((columns, group) => matched[group] === "1" && columns.has(column)));
|
|
392
|
+
hidden = {
|
|
393
|
+
cleared: columns.filter((c) => !primaryKey.includes(c)),
|
|
394
|
+
primaryKey: columns.some((c) => primaryKey.includes(c))
|
|
395
|
+
};
|
|
396
|
+
mask.hiddenPerMatch.set(matched, hidden);
|
|
397
|
+
}
|
|
398
|
+
return hidden;
|
|
399
|
+
}
|
|
400
|
+
const hasWhere = (where) => !!where && Object.keys(where).length > 0;
|
|
401
|
+
/** Columns every returned row is allowed to expose. */
|
|
402
|
+
function guaranteedColumns(rules, allColumnNames) {
|
|
403
|
+
const guaranteed = /* @__PURE__ */ new Set();
|
|
404
|
+
for (const rule of rules) {
|
|
405
|
+
if (hasWhere(rule.where)) continue;
|
|
406
|
+
for (const column of rule.columns) guaranteed.add(column);
|
|
407
|
+
}
|
|
408
|
+
for (const column of allColumnNames) if (rules.every((r) => r.columns.has(column))) guaranteed.add(column);
|
|
409
|
+
return guaranteed;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Groups the conditional rules by the columns they grant on top of the
|
|
413
|
+
* guaranteed ones. Each group gets its own flag.
|
|
414
|
+
*/
|
|
415
|
+
function conditionalGroups(rules, guaranteed) {
|
|
416
|
+
const groups = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const rule of rules) {
|
|
418
|
+
if (!hasWhere(rule.where)) continue;
|
|
419
|
+
const additional = [...rule.columns].filter((c) => !guaranteed.has(c)).sort();
|
|
420
|
+
if (additional.length === 0) continue;
|
|
421
|
+
const key = additional.join(",");
|
|
422
|
+
const group = groups.get(key);
|
|
423
|
+
if (group) group.wheres.push(rule.where);
|
|
424
|
+
else groups.set(key, {
|
|
425
|
+
columns: new Set(additional),
|
|
426
|
+
wheres: [rule.where]
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return [...groups.values()];
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Creates the resolver which turns the abilities of a table into the columns
|
|
433
|
+
* to select and, if columns depend on the matched ability, extras flagging
|
|
434
|
+
* per row which abilities matched plus the mask to decode them.
|
|
435
|
+
*/
|
|
436
|
+
function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
437
|
+
const allColumnNames = Object.keys(tableSchema.columns);
|
|
438
|
+
const primaryKeyNames = Object.keys(tableSchema.primaryKey);
|
|
439
|
+
const primaryKeyName = primaryKeyNames[0];
|
|
440
|
+
const toSQL = (table, wheres) => (0, drizzle_orm.or)(...wheres.map((where) => (0, drizzle_orm.relationsFilterToSQL)(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
|
|
441
|
+
const uncorrelated = (table, wheres, group) => {
|
|
442
|
+
const inner = (0, drizzle_orm.aliasedTable)(tableSchema.table, `rumble_columns_${group}`);
|
|
443
|
+
return drizzle_orm.sql`${table[primaryKeyName]} in (${db.select({ pk: inner[primaryKeyName] }).from(inner).where(toSQL(inner, wheres))})`;
|
|
444
|
+
};
|
|
445
|
+
return function resolveColumnAccess(action, filters) {
|
|
446
|
+
const rules = filters.map((f) => ({
|
|
447
|
+
where: realWhere(f?.where),
|
|
448
|
+
columns: selectedColumns(f?.columns, allColumnNames)
|
|
449
|
+
}));
|
|
450
|
+
const guaranteed = guaranteedColumns(rules, allColumnNames);
|
|
451
|
+
const groups = conditionalGroups(rules, guaranteed);
|
|
452
|
+
const selected = /* @__PURE__ */ new Set([...primaryKeyNames, ...rules.flatMap((r) => [...r.columns])]);
|
|
453
|
+
const columns = selected.size === allColumnNames.length ? void 0 : Object.fromEntries([...selected].map((c) => [c, true]));
|
|
454
|
+
if (guaranteed.size === allColumnNames.length) return {
|
|
455
|
+
columns,
|
|
456
|
+
extras: void 0,
|
|
457
|
+
mask: void 0
|
|
458
|
+
};
|
|
459
|
+
const mask = {
|
|
460
|
+
guaranteed,
|
|
461
|
+
conditional: groups.map((g) => g.columns),
|
|
462
|
+
flags: groups.map((_, group) => columnFlagKey(group)),
|
|
463
|
+
hiddenPerMatch: /* @__PURE__ */ new Map()
|
|
464
|
+
};
|
|
465
|
+
const actionIndex = drizzle_orm.sql.raw(String(actions.indexOf(action)));
|
|
466
|
+
const extras = { [ACTION_FLAG_KEY]: () => drizzle_orm.sql`${actionIndex}` };
|
|
467
|
+
groups.forEach(({ wheres }, group) => {
|
|
468
|
+
const relational = wheres.some((w) => referencesRelation(w, tableSchema.relations));
|
|
469
|
+
extras[mask.flags[group]] = (table) => {
|
|
470
|
+
const condition = relational ? uncorrelated(table, wheres, group) : toSQL(table, wheres);
|
|
471
|
+
return drizzle_orm.sql`case when ${condition} then 1 else 0 end`;
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
columns,
|
|
476
|
+
extras,
|
|
477
|
+
mask
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const isRow = (row) => !!row && typeof row === "object";
|
|
482
|
+
function clearColumns(row, columns) {
|
|
483
|
+
for (let i = 0; i < columns.length; i++) {
|
|
484
|
+
const column = columns[i];
|
|
485
|
+
if (row[column] !== void 0) row[column] = void 0;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function matchedGroups(row, flags) {
|
|
489
|
+
let matched = "";
|
|
490
|
+
for (let i = 0; i < flags.length; i++) matched += Number(row[flags[i]]) === 1 ? "1" : "0";
|
|
491
|
+
return matched;
|
|
492
|
+
}
|
|
493
|
+
function maskRow(row, mask, hidden) {
|
|
494
|
+
if (row["__rumble_action"] !== void 0) row[ACTION_FLAG_KEY] = void 0;
|
|
495
|
+
clearColumns(row, mask.flags);
|
|
496
|
+
clearColumns(row, hidden.cleared);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Creates the function hiding the columns of queried rows which the ability
|
|
500
|
+
* that matched the row does not grant.
|
|
501
|
+
*/
|
|
502
|
+
function createColumnMasker({ actions }, tables) {
|
|
503
|
+
const stateOf = (abilities) => requestStateOf(abilities);
|
|
504
|
+
const maskedActions = (abilities, table) => {
|
|
505
|
+
const { actions } = stateOf(abilities);
|
|
506
|
+
let used = actions.get(table);
|
|
507
|
+
if (!used) {
|
|
508
|
+
used = /* @__PURE__ */ new Set();
|
|
509
|
+
actions.set(table, used);
|
|
510
|
+
}
|
|
511
|
+
return used;
|
|
512
|
+
};
|
|
513
|
+
const primaryKeyHidden = (abilities, row) => stateOf(abilities).masked.get(row) === true;
|
|
514
|
+
const maskColumns = ({ table, action, abilities, entities }, start = 0) => {
|
|
515
|
+
const tableAbilities = abilities[table];
|
|
516
|
+
const { columns: columnNames, primaryKey } = tables.get(table);
|
|
517
|
+
const state = stateOf(abilities);
|
|
518
|
+
const used = maskedActions(abilities, table);
|
|
519
|
+
for (let i = start; i < entities.length; i++) {
|
|
520
|
+
const row = entities[i];
|
|
521
|
+
if (!isRow(row) || state.masked.has(row)) continue;
|
|
522
|
+
const rowAction = row["__rumble_action"] != null ? actions[Number(row[ACTION_FLAG_KEY])] : action;
|
|
523
|
+
used.add(rowAction);
|
|
524
|
+
const resolved = tableAbilities[resolvedFilterKey](rowAction);
|
|
525
|
+
if (!resolved) return tableAbilities.filter(rowAction).then(() => maskColumns({
|
|
526
|
+
table,
|
|
527
|
+
action,
|
|
528
|
+
abilities,
|
|
529
|
+
entities
|
|
530
|
+
}, i));
|
|
531
|
+
const mask = resolved[columnMaskKey];
|
|
532
|
+
if (!mask) continue;
|
|
533
|
+
const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
|
|
534
|
+
maskRow(row, mask, hidden);
|
|
535
|
+
state.masked.set(row, hidden.primaryKey);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
return {
|
|
539
|
+
maskColumns,
|
|
540
|
+
maskedActions,
|
|
541
|
+
primaryKeyHidden
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
296
545
|
//#region lib/helpers/sqlTypes/types.ts
|
|
297
546
|
/**
|
|
298
547
|
* Strips parameter suffixes like `(256)`, `(10,2)` and timezone modifiers
|
|
@@ -438,76 +687,6 @@ function createDistinctValuesFromSQLType(sqlType) {
|
|
|
438
687
|
throw UnknownTypeRumbleError(sqlType, "Distinct");
|
|
439
688
|
}
|
|
440
689
|
//#endregion
|
|
441
|
-
//#region lib/helpers/tableHelpers.ts
|
|
442
|
-
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
443
|
-
function buildIndex(db) {
|
|
444
|
-
const byTsName = /* @__PURE__ */ new Map();
|
|
445
|
-
const byDbName = /* @__PURE__ */ new Map();
|
|
446
|
-
const byTableRef = /* @__PURE__ */ new Map();
|
|
447
|
-
for (const [tsName, rawEntry] of Object.entries(db._.relations ?? {})) {
|
|
448
|
-
const entry = rawEntry;
|
|
449
|
-
const tableObj = entry.table;
|
|
450
|
-
if (!tableObj) continue;
|
|
451
|
-
const columns = (0, drizzle_orm.isTable)(tableObj) ? (0, drizzle_orm.getTableColumns)(tableObj) : {};
|
|
452
|
-
const primaryKey = Object.fromEntries(Object.entries(columns).filter(([, col]) => col.primary));
|
|
453
|
-
const dbName = (0, drizzle_orm.isTable)(tableObj) ? (0, drizzle_orm.getTableName)(tableObj) : entry.name ?? tsName;
|
|
454
|
-
const resolved = {
|
|
455
|
-
name: entry.name ?? tsName,
|
|
456
|
-
dbName,
|
|
457
|
-
table: tableObj,
|
|
458
|
-
columns,
|
|
459
|
-
primaryKey,
|
|
460
|
-
relations: entry.relations ?? {},
|
|
461
|
-
isTable: (0, drizzle_orm.isTable)(tableObj),
|
|
462
|
-
foundRelation: entry
|
|
463
|
-
};
|
|
464
|
-
byTsName.set(resolved.name, resolved);
|
|
465
|
-
byDbName.set(resolved.dbName, resolved);
|
|
466
|
-
byTableRef.set(tableObj, resolved);
|
|
467
|
-
}
|
|
468
|
-
return {
|
|
469
|
-
byTsName,
|
|
470
|
-
byDbName,
|
|
471
|
-
byTableRef
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
function getIndex(db) {
|
|
475
|
-
let idx = indexCache.get(db);
|
|
476
|
-
if (!idx) {
|
|
477
|
-
idx = buildIndex(db);
|
|
478
|
-
indexCache.set(db, idx);
|
|
479
|
-
}
|
|
480
|
-
return idx;
|
|
481
|
-
}
|
|
482
|
-
function tableHelper({ db, table }) {
|
|
483
|
-
const idx = getIndex(db);
|
|
484
|
-
let entry;
|
|
485
|
-
if (typeof table === "string") entry = idx.byTsName.get(table) ?? idx.byDbName.get(table);
|
|
486
|
-
else if (table && typeof table === "object") {
|
|
487
|
-
const t = table;
|
|
488
|
-
if (t.table) {
|
|
489
|
-
entry = idx.byTableRef.get(t.table);
|
|
490
|
-
if (!entry && t.name) entry = idx.byTsName.get(t.name) ?? idx.byDbName.get(t.name);
|
|
491
|
-
}
|
|
492
|
-
if (!entry) entry = idx.byTableRef.get(t);
|
|
493
|
-
if (!entry && (0, drizzle_orm.isTable)(t)) {
|
|
494
|
-
const dbName = (0, drizzle_orm.getTableName)(t);
|
|
495
|
-
entry = idx.byDbName.get(dbName);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (!entry) throw new RumbleError(`Could not find schema for ${JSON.stringify(typeof table === "string" ? table : "<table-object>")}`);
|
|
499
|
-
return {
|
|
500
|
-
columns: entry.columns,
|
|
501
|
-
primaryKey: entry.primaryKey,
|
|
502
|
-
relations: entry.relations,
|
|
503
|
-
dbName: entry.dbName,
|
|
504
|
-
tsName: entry.name,
|
|
505
|
-
table: entry.table,
|
|
506
|
-
isTable: entry.isTable,
|
|
507
|
-
foundRelation: entry.foundRelation
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
//#endregion
|
|
511
690
|
//#region lib/helpers/telemetry.ts
|
|
512
691
|
const SPAN_EXECUTE = _pothos_tracing_opentelemetry.SpanNames.EXECUTE;
|
|
513
692
|
/** @pothos/tracing-opentelemetry's SpanNames enum has no subscribe member. */
|
|
@@ -683,13 +862,161 @@ function durationMs(start) {
|
|
|
683
862
|
return Math.round((performance.now() - start) * 100) / 100;
|
|
684
863
|
}
|
|
685
864
|
//#endregion
|
|
686
|
-
//#region lib/abilityBuilder.ts
|
|
865
|
+
//#region lib/abilityBuilder/filterResponse.ts
|
|
866
|
+
/**
|
|
867
|
+
* The limit of a find many call. An injected merge limit wins unless the
|
|
868
|
+
* ability limit is lower, otherwise the default limit caps the ability limit.
|
|
869
|
+
*/
|
|
870
|
+
function resolveLimit(abilityLimit, mergedLimit, defaultLimit) {
|
|
871
|
+
if (mergedLimit !== void 0 && (!abilityLimit || abilityLimit > mergedLimit)) return mergedLimit;
|
|
872
|
+
if (defaultLimit && (abilityLimit === void 0 || abilityLimit > defaultLimit)) return defaultLimit;
|
|
873
|
+
return abilityLimit ?? void 0;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Creates the function packing resolved query filters into the object handed
|
|
877
|
+
* out by `abilities.<table>.filter(...)`.
|
|
878
|
+
*/
|
|
879
|
+
function createFilterResponder({ db, defaultLimit }, tableSchema) {
|
|
880
|
+
const internalTransformer = (filters, mergedLimit) => {
|
|
881
|
+
const where = filters?.where;
|
|
882
|
+
const extras = filters?.extras;
|
|
883
|
+
const columns = filters?.columns;
|
|
884
|
+
const limit = require_lazy.lazy(() => resolveLimit(filters?.limit, mergedLimit, defaultLimit));
|
|
885
|
+
const sqlTransformedWhere = require_lazy.lazy(() => {
|
|
886
|
+
const casing = db._.session?.dialect?.casing;
|
|
887
|
+
return where ? (0, drizzle_orm.relationsFilterToSQL)(tableSchema.foundRelation.table, sanitizeFilterValue(where), tableSchema.relations, db._.relations, casing) : void 0;
|
|
888
|
+
});
|
|
889
|
+
return {
|
|
890
|
+
query: {
|
|
891
|
+
single: columns ? {
|
|
892
|
+
extras,
|
|
893
|
+
where: sanitizeFilterValue(where),
|
|
894
|
+
columns
|
|
895
|
+
} : {
|
|
896
|
+
extras,
|
|
897
|
+
where: sanitizeFilterValue(where)
|
|
898
|
+
},
|
|
899
|
+
many: columns ? {
|
|
900
|
+
extras,
|
|
901
|
+
where: sanitizeFilterValue(where),
|
|
902
|
+
columns,
|
|
903
|
+
get limit() {
|
|
904
|
+
return limit();
|
|
905
|
+
}
|
|
906
|
+
} : {
|
|
907
|
+
extras,
|
|
908
|
+
where: sanitizeFilterValue(where),
|
|
909
|
+
get limit() {
|
|
910
|
+
return limit();
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
sql: { get where() {
|
|
915
|
+
return sqlTransformedWhere();
|
|
916
|
+
} }
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
/**
|
|
920
|
+
* Packs the filters into a response object that can be applied for queries by the user
|
|
921
|
+
*/
|
|
922
|
+
return function transformToResponse(queryFilters, columnMask) {
|
|
923
|
+
const ret = internalTransformer(queryFilters);
|
|
924
|
+
ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
|
|
925
|
+
ret[columnMaskKey] = columnMask;
|
|
926
|
+
return ret;
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region lib/abilityBuilder/queryFilter.ts
|
|
687
931
|
function isDynamicQueryFilter(filter) {
|
|
688
932
|
return typeof filter === "function";
|
|
689
933
|
}
|
|
690
934
|
function isStaticQueryFilter(filter) {
|
|
691
935
|
return typeof filter !== "function";
|
|
692
936
|
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region lib/helpers/errorLogging.ts
|
|
939
|
+
function hasMessage(error) {
|
|
940
|
+
return typeof error === "object" && error !== null && "message" in error;
|
|
941
|
+
}
|
|
942
|
+
function toLoggedException(error) {
|
|
943
|
+
if (hasMessage(error)) {
|
|
944
|
+
const gqlError = error;
|
|
945
|
+
return {
|
|
946
|
+
"exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
|
|
947
|
+
"exception.message": String(gqlError.message),
|
|
948
|
+
...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
|
|
949
|
+
...gqlError.path ? { path: gqlError.path } : {},
|
|
950
|
+
...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
"exception.type": "UnknownError",
|
|
955
|
+
"exception.message": String(error)
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Every erroneous rumble log carries the same `errors` array shape,
|
|
960
|
+
* whether it originates from a thrown exception (single-element array)
|
|
961
|
+
* or a GraphQL `ExecutionResult.errors` list (multi-element array).
|
|
962
|
+
*/
|
|
963
|
+
function errorsLogField(errors) {
|
|
964
|
+
return { errors: errors.map(toLoggedException) };
|
|
965
|
+
}
|
|
966
|
+
function errorLogField(error) {
|
|
967
|
+
return errorsLogField([error]);
|
|
968
|
+
}
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region lib/abilityBuilder/telemetry.ts
|
|
971
|
+
const makeNothingRegisteredWarner = (config, logger) => (0, es_toolkit.debounce)((model, action) => {
|
|
972
|
+
const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
|
|
973
|
+
if (logger) logger.warn({
|
|
974
|
+
[ATTR_TABLE]: model,
|
|
975
|
+
[ATTR_ACTION]: action,
|
|
976
|
+
...traceCorrelationFields(config)
|
|
977
|
+
}, msg);
|
|
978
|
+
else console.warn(msg);
|
|
979
|
+
}, 1e3);
|
|
980
|
+
/**
|
|
981
|
+
* Runs the preparation of the abilities of one table and action, reporting
|
|
982
|
+
* the attributes it collects to the span and the logger.
|
|
983
|
+
*/
|
|
984
|
+
function traceAbilityPreparation({ otel, log, telemetryConfig }, tableName, action, assemble) {
|
|
985
|
+
const run = async (span) => {
|
|
986
|
+
const attributes = {
|
|
987
|
+
[ATTR_TABLE]: tableName,
|
|
988
|
+
[ATTR_ACTION]: action
|
|
989
|
+
};
|
|
990
|
+
try {
|
|
991
|
+
const result = await assemble(attributes);
|
|
992
|
+
log?.debug({
|
|
993
|
+
...attributes,
|
|
994
|
+
...traceCorrelationFields(telemetryConfig, span)
|
|
995
|
+
}, "abilities prepared");
|
|
996
|
+
return result;
|
|
997
|
+
} catch (error) {
|
|
998
|
+
recordSpanError(span, error);
|
|
999
|
+
log?.error({
|
|
1000
|
+
...attributes,
|
|
1001
|
+
...traceCorrelationFields(telemetryConfig, span),
|
|
1002
|
+
...errorLogField(error)
|
|
1003
|
+
}, "abilities failed");
|
|
1004
|
+
throw error;
|
|
1005
|
+
} finally {
|
|
1006
|
+
span?.setAttributes(attributes);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
|
|
1010
|
+
try {
|
|
1011
|
+
return await run(span);
|
|
1012
|
+
} finally {
|
|
1013
|
+
span.end();
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
return run();
|
|
1017
|
+
}
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region lib/abilityBuilder/tableAbilities.ts
|
|
693
1020
|
/**
|
|
694
1021
|
* filter() is async. Without type checking (plain JS, `any`) a forgotten await would
|
|
695
1022
|
* hand the promise itself to drizzle
|
|
@@ -707,114 +1034,223 @@ function guardAgainstMissingAwait(promise) {
|
|
|
707
1034
|
});
|
|
708
1035
|
return promise;
|
|
709
1036
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
},
|
|
1037
|
+
/**
|
|
1038
|
+
* A filter no row can match, since it requires the primary key to equal two
|
|
1039
|
+
* distinct values at once.
|
|
1040
|
+
*/
|
|
1041
|
+
function createBlockEverythingFilter(tableName, tableSchema) {
|
|
1042
|
+
if (Object.keys(tableSchema.primaryKey).length === 0) throw new RumbleError(`No primary key found for entity ${tableName}`);
|
|
1043
|
+
const [primaryKeyName, primaryKeyField] = Object.entries(tableSchema.primaryKey)[0];
|
|
1044
|
+
const distinctValues = createDistinctValuesFromSQLType(primaryKeyField.getSQLType());
|
|
1045
|
+
return { where: { AND: [{ [primaryKeyName]: distinctValues.value1 }, { [primaryKeyName]: distinctValues.value2 }] } };
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Creates the abilities of a single table, which resolve the registered
|
|
1049
|
+
* query filters for a request.
|
|
1050
|
+
*/
|
|
1051
|
+
const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
1052
|
+
const { db, actions } = settings;
|
|
1053
|
+
const tableSchema = tableHelper({
|
|
1054
|
+
db,
|
|
1055
|
+
table: tableName
|
|
1056
|
+
});
|
|
1057
|
+
const withRestrictingWhere = (filter) => {
|
|
1058
|
+
const where = realWhere(filter.where);
|
|
1059
|
+
return !!where && Object.keys(where).length > 0 && (0, drizzle_orm.relationsFilterToSQL)(tableSchema.table, sanitizeFilterValue(where), tableSchema.relations, db._.relations) !== void 0 || where === void 0 ? filter : {
|
|
1060
|
+
...filter,
|
|
1061
|
+
where: void 0
|
|
1062
|
+
};
|
|
1063
|
+
};
|
|
1064
|
+
const simpleQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
1065
|
+
const filters = queryFilters.get(action);
|
|
1066
|
+
if (!filters || filters === "unrestricted") return [action, []];
|
|
1067
|
+
return [action, filters.filter(isStaticQueryFilter).map(withRestrictingWhere)];
|
|
1068
|
+
}));
|
|
1069
|
+
const dynamicQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
1070
|
+
const filters = queryFilters.get(action);
|
|
1071
|
+
if (!filters || filters === "unrestricted") return [action, []];
|
|
1072
|
+
return [action, filters.filter(isDynamicQueryFilter)];
|
|
1073
|
+
}));
|
|
1074
|
+
const blockEverythingFilter = createBlockEverythingFilter(String(tableName), tableSchema);
|
|
1075
|
+
const resolveColumnAccess = createColumnAccessResolver(settings, tableSchema);
|
|
1076
|
+
const transformToResponse = createFilterResponder(settings, tableSchema);
|
|
1077
|
+
const assembleAbilities = async (userContext, action, attributes) => {
|
|
1078
|
+
const filters = queryFilters.get(action);
|
|
1079
|
+
if (filters === "unrestricted") {
|
|
1080
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1081
|
+
return transformToResponse();
|
|
1082
|
+
}
|
|
1083
|
+
if (!filters) {
|
|
1084
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1085
|
+
settings.warnNothingRegistered(String(tableName), action);
|
|
1086
|
+
return transformToResponse(blockEverythingFilter);
|
|
1087
|
+
}
|
|
1088
|
+
const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
|
|
1089
|
+
if (rawResults.includes("allow")) {
|
|
1090
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1091
|
+
return transformToResponse();
|
|
1092
|
+
}
|
|
1093
|
+
const dynamicResults = rawResults.filter((r) => r !== void 0).map(withRestrictingWhere);
|
|
1094
|
+
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
1095
|
+
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
1096
|
+
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
1097
|
+
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
1098
|
+
if (allQueryFilters.length === 0) {
|
|
1099
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1100
|
+
return transformToResponse(blockEverythingFilter);
|
|
1101
|
+
}
|
|
1102
|
+
const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
|
|
1103
|
+
return mergeFilters(a, b, "OR");
|
|
1104
|
+
});
|
|
1105
|
+
const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
|
|
1106
|
+
if (columns) mergedFilters.columns = columns;
|
|
1107
|
+
else delete mergedFilters.columns;
|
|
1108
|
+
if (extras) mergedFilters.extras = {
|
|
1109
|
+
...mergedFilters.extras,
|
|
1110
|
+
...extras
|
|
1111
|
+
};
|
|
1112
|
+
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
1113
|
+
return transformToResponse(mergedFilters, mask);
|
|
1114
|
+
};
|
|
1115
|
+
return { withContext: (userContext) => {
|
|
1116
|
+
const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
|
|
1117
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1118
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1119
|
+
const abilities = { filter: (action) => {
|
|
1120
|
+
let prepared = cache.get(action);
|
|
1121
|
+
if (!prepared) {
|
|
1122
|
+
prepared = guardAgainstMissingAwait(prepare(action));
|
|
1123
|
+
cache.set(action, prepared);
|
|
1124
|
+
prepared.then((result) => resolved.set(action, result), () => {});
|
|
1125
|
+
}
|
|
1126
|
+
return prepared;
|
|
1127
|
+
} };
|
|
1128
|
+
abilities[resolvedFilterKey] = (action) => resolved.get(action);
|
|
1129
|
+
return abilities;
|
|
1130
|
+
} };
|
|
1131
|
+
};
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region lib/abilityBuilder/tableBuilder.ts
|
|
1134
|
+
/**
|
|
1135
|
+
* Creates the registration API (allow/filter) of a single table.
|
|
1136
|
+
*/
|
|
1137
|
+
const createTableBuilder = ({ actions, isBuilt }) => {
|
|
1138
|
+
const queryFilters = /* @__PURE__ */ new Map();
|
|
1139
|
+
const runtimeFilters = /* @__PURE__ */ new Map();
|
|
1140
|
+
for (const action of actions) if (!runtimeFilters.has(action)) runtimeFilters.set(action, []);
|
|
1141
|
+
const registerFilter = (actions, prefetch) => ({
|
|
1142
|
+
/**
|
|
1143
|
+
* The actual filter function to apply. Returns the allowed values
|
|
1144
|
+
*/
|
|
1145
|
+
by: (explicitFilter) => {
|
|
1146
|
+
for (const action of actions) runtimeFilters.get(action).push(prefetch ? {
|
|
1147
|
+
filter: explicitFilter,
|
|
1148
|
+
prefetch
|
|
1149
|
+
} : { filter: explicitFilter });
|
|
1150
|
+
} });
|
|
1151
|
+
return {
|
|
1152
|
+
/**
|
|
1153
|
+
* Allows to perform a specific action on a specific entity
|
|
1154
|
+
*/
|
|
1155
|
+
allow: (action) => {
|
|
1156
|
+
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.");
|
|
1157
|
+
const actions = Array.isArray(action) ? action : [action];
|
|
1158
|
+
const setUnrestrictedByThisCall = /* @__PURE__ */ new Set();
|
|
1159
|
+
for (const action of actions) {
|
|
1160
|
+
let filters = queryFilters.get(action);
|
|
1161
|
+
if (!filters) {
|
|
1162
|
+
filters = "unrestricted";
|
|
1163
|
+
queryFilters.set(action, filters);
|
|
1164
|
+
setUnrestrictedByThisCall.add(action);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
/**
|
|
1169
|
+
* Restricts the allowed actions to a filter
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```ts
|
|
1172
|
+
* abilityBuilder.users.allow(["read", "update", "delete"]).when(({ userId }) => ({
|
|
1173
|
+
* where: {
|
|
1174
|
+
* id: userId,
|
|
1175
|
+
* },
|
|
1176
|
+
* }));
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
when: (queryFilter) => {
|
|
1180
|
+
for (const action of actions) {
|
|
1181
|
+
if (queryFilters.get(action) === "unrestricted") {
|
|
1182
|
+
if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
|
|
1183
|
+
else continue;
|
|
1184
|
+
}
|
|
1185
|
+
queryFilters.get(action).push(queryFilter);
|
|
1186
|
+
}
|
|
1187
|
+
} };
|
|
1188
|
+
},
|
|
1189
|
+
/**
|
|
1190
|
+
* Allows to register an application level filter to restrict some results
|
|
1191
|
+
* which were returned by a query
|
|
1192
|
+
*/
|
|
1193
|
+
filter: (action) => {
|
|
1194
|
+
const actions = Array.isArray(action) ? action : [action];
|
|
1195
|
+
return {
|
|
1196
|
+
/**
|
|
1197
|
+
* Allows to register an application level prefetch to fetch some data
|
|
1198
|
+
* which could be useful for later filtering the results. The prefetch
|
|
1199
|
+
* function will be called with the user context but unlike the actual
|
|
1200
|
+
* filter function, it will not have access to the result of the query
|
|
1201
|
+
* and therefore can be run in parallel with underlying query resolver.
|
|
1202
|
+
* A typical use case is to fetch some data which is not directly
|
|
1203
|
+
* related to the query but to the context only. So e.g. fetching the
|
|
1204
|
+
* user's permissions from an external system and then later applying
|
|
1205
|
+
* the filter based on those permissions.
|
|
1206
|
+
*/
|
|
1207
|
+
prefetch: (prefetch) => registerFilter(actions, prefetch),
|
|
1208
|
+
...registerFilter(actions)
|
|
1209
|
+
};
|
|
1210
|
+
},
|
|
1211
|
+
_: {
|
|
1212
|
+
runtimeFilters,
|
|
1213
|
+
queryFilters
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
};
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region lib/abilityBuilder/index.ts
|
|
719
1219
|
const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
|
|
720
1220
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
721
1221
|
const telemetryConfig = {
|
|
722
1222
|
otel,
|
|
723
1223
|
logger: loggerConfig
|
|
724
1224
|
};
|
|
725
|
-
const
|
|
1225
|
+
const settings = {
|
|
1226
|
+
db,
|
|
1227
|
+
actions,
|
|
1228
|
+
defaultLimit,
|
|
1229
|
+
otel,
|
|
1230
|
+
telemetryConfig,
|
|
1231
|
+
log,
|
|
1232
|
+
warnNothingRegistered: makeNothingRegisteredWarner(telemetryConfig, log)
|
|
1233
|
+
};
|
|
726
1234
|
const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
|
|
727
1235
|
db,
|
|
728
1236
|
table: name
|
|
729
1237
|
}).isTable);
|
|
730
1238
|
let hasBeenBuilt = false;
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
filters = "unrestricted";
|
|
747
|
-
queryFilters.set(action, filters);
|
|
748
|
-
setUnrestrictedByThisCall.add(action);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
return {
|
|
752
|
-
/**
|
|
753
|
-
* Restricts the allowed actions to a filter
|
|
754
|
-
* @example
|
|
755
|
-
* ```ts
|
|
756
|
-
* abilityBuilder.users.allow(["read", "update", "delete"]).when(({ userId }) => ({
|
|
757
|
-
* where: {
|
|
758
|
-
* id: userId,
|
|
759
|
-
* },
|
|
760
|
-
* }));
|
|
761
|
-
* ```
|
|
762
|
-
*/
|
|
763
|
-
when: (queryFilter) => {
|
|
764
|
-
for (const action of actions) {
|
|
765
|
-
if (queryFilters.get(action) === "unrestricted") {
|
|
766
|
-
if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
|
|
767
|
-
else continue;
|
|
768
|
-
}
|
|
769
|
-
queryFilters.get(action).push(queryFilter);
|
|
770
|
-
}
|
|
771
|
-
} };
|
|
772
|
-
},
|
|
773
|
-
/**
|
|
774
|
-
* Allows to register an application level filter to restrict some results
|
|
775
|
-
* which were returned by a query
|
|
776
|
-
*/
|
|
777
|
-
filter: (action) => {
|
|
778
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
779
|
-
return {
|
|
780
|
-
/**
|
|
781
|
-
* Allows to register an application level prefetch to fetch some data
|
|
782
|
-
* which could be useful for later filtering the results. The prefetch
|
|
783
|
-
* function will be called with the user context but unlike the actual
|
|
784
|
-
* filter function, it will not have access to the result of the query
|
|
785
|
-
* and therefore can be run in parallel with underlying query resolver.
|
|
786
|
-
* A typical use case is to fetch some data which is not directly
|
|
787
|
-
* related to the query but to the context only. So e.g. fetching the
|
|
788
|
-
* user's permissions from an external system and then later applying
|
|
789
|
-
* the filter based on those permissions.
|
|
790
|
-
*/
|
|
791
|
-
prefetch: (prefetch) => {
|
|
792
|
-
return {
|
|
793
|
-
/**
|
|
794
|
-
* The actual filter function to apply. Returns the allowed values
|
|
795
|
-
*/
|
|
796
|
-
by: (explicitFilter) => {
|
|
797
|
-
for (const action of actions) runtimeFilters.get(action).push({
|
|
798
|
-
filter: explicitFilter,
|
|
799
|
-
prefetch
|
|
800
|
-
});
|
|
801
|
-
} };
|
|
802
|
-
},
|
|
803
|
-
/**
|
|
804
|
-
* The actual filter function to apply. Returns the allowed values
|
|
805
|
-
*/
|
|
806
|
-
by: (explicitFilter) => {
|
|
807
|
-
for (const action of actions) runtimeFilters.get(action).push({ filter: explicitFilter });
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
},
|
|
811
|
-
_: {
|
|
812
|
-
runtimeFilters,
|
|
813
|
-
queryFilters
|
|
814
|
-
}
|
|
815
|
-
};
|
|
816
|
-
};
|
|
817
|
-
const buildersPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createBuilderForTable()]));
|
|
1239
|
+
const buildersPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createTableBuilder({
|
|
1240
|
+
actions: settings.actions,
|
|
1241
|
+
isBuilt: () => hasBeenBuilt
|
|
1242
|
+
})]));
|
|
1243
|
+
const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1244
|
+
const tableSchema = tableHelper({
|
|
1245
|
+
db,
|
|
1246
|
+
table: tableName
|
|
1247
|
+
});
|
|
1248
|
+
return [tableName, {
|
|
1249
|
+
columns: Object.keys(tableSchema.columns),
|
|
1250
|
+
primaryKey: Object.keys(tableSchema.primaryKey)
|
|
1251
|
+
}];
|
|
1252
|
+
})));
|
|
1253
|
+
const readActionPerTable = /* @__PURE__ */ new Map();
|
|
818
1254
|
return {
|
|
819
1255
|
...buildersPerTable,
|
|
820
1256
|
/**
|
|
@@ -822,247 +1258,28 @@ by: (explicitFilter) => {
|
|
|
822
1258
|
* @ignore
|
|
823
1259
|
*/
|
|
824
1260
|
_: {
|
|
1261
|
+
maskColumns: (input) => maskColumns(input),
|
|
1262
|
+
/** The actions rows of the table were masked with in this request. */
|
|
1263
|
+
maskedActions: (abilities, table) => maskedActions(abilities, table),
|
|
1264
|
+
/** Whether masking in this request hid the primary key of the row. */
|
|
1265
|
+
primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
|
|
1266
|
+
registerReadAction(table, action) {
|
|
1267
|
+
readActionPerTable.set(table, action);
|
|
1268
|
+
},
|
|
1269
|
+
readActionOf(table) {
|
|
1270
|
+
return readActionPerTable.get(table) ?? "read";
|
|
1271
|
+
},
|
|
825
1272
|
registeredFilters({ action, table }) {
|
|
826
1273
|
return buildersPerTable[table]._.runtimeFilters.get(action);
|
|
827
1274
|
},
|
|
828
1275
|
build() {
|
|
829
|
-
const
|
|
830
|
-
const queryFilters = buildersPerTable[tableName]._.queryFilters;
|
|
831
|
-
const simpleQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
832
|
-
const filters = queryFilters.get(action);
|
|
833
|
-
if (!filters || filters === "unrestricted") return [action, []];
|
|
834
|
-
return [action, filters.filter(isStaticQueryFilter)];
|
|
835
|
-
}));
|
|
836
|
-
const dynamicQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
837
|
-
const filters = queryFilters.get(action);
|
|
838
|
-
if (!filters || filters === "unrestricted") return [action, []];
|
|
839
|
-
return [action, filters.filter(isDynamicQueryFilter)];
|
|
840
|
-
}));
|
|
841
|
-
const tableSchema = tableHelper({
|
|
842
|
-
db,
|
|
843
|
-
table: tableName
|
|
844
|
-
});
|
|
845
|
-
if (Object.keys(tableSchema.primaryKey).length === 0) throw new RumbleError(`No primary key found for entity ${String(tableName)}`);
|
|
846
|
-
const primaryKeyField = Object.values(tableSchema.primaryKey)[0];
|
|
847
|
-
const distinctValues = createDistinctValuesFromSQLType(primaryKeyField.getSQLType());
|
|
848
|
-
const blockEverythingFilter = { where: { AND: [{ [primaryKeyField.name]: distinctValues.value1 }, { [primaryKeyField.name]: distinctValues.value2 }] } };
|
|
849
|
-
/**
|
|
850
|
-
* Packs the filters into a response object that can be applied for queries by the user
|
|
851
|
-
*/
|
|
852
|
-
function transformToResponse(queryFilters) {
|
|
853
|
-
const internalTransformer = (filters, mergedLimit) => {
|
|
854
|
-
const limit = require_lazy.lazy(() => {
|
|
855
|
-
if (mergedLimit !== void 0) {
|
|
856
|
-
if (!filters?.limit) return mergedLimit;
|
|
857
|
-
if (filters.limit > mergedLimit) return mergedLimit;
|
|
858
|
-
}
|
|
859
|
-
let limit = filters?.limit;
|
|
860
|
-
if (defaultLimit && (limit === void 0 || limit > defaultLimit)) limit = defaultLimit;
|
|
861
|
-
return limit ?? void 0;
|
|
862
|
-
});
|
|
863
|
-
const sqlTransformedWhere = require_lazy.lazy(() => {
|
|
864
|
-
const casing = db._.session?.dialect?.casing;
|
|
865
|
-
return filters?.where ? (0, drizzle_orm.relationsFilterToSQL)(tableSchema.foundRelation.table, sanitizeFilterValue(filters.where), tableSchema.relations, db._.relations, casing) : void 0;
|
|
866
|
-
});
|
|
867
|
-
if (filters?.columns) return {
|
|
868
|
-
/**
|
|
869
|
-
* Query filters for the drizzle query API.
|
|
870
|
-
* @example
|
|
871
|
-
* ```ts
|
|
872
|
-
* author: t.relation("author", {
|
|
873
|
-
* query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
|
|
874
|
-
* }),
|
|
875
|
-
* ´´´
|
|
876
|
-
*/
|
|
877
|
-
query: {
|
|
878
|
-
/**
|
|
879
|
-
* For find first calls
|
|
880
|
-
*/
|
|
881
|
-
single: {
|
|
882
|
-
extras: filters?.extras,
|
|
883
|
-
where: sanitizeFilterValue(filters?.where),
|
|
884
|
-
columns: filters?.columns
|
|
885
|
-
},
|
|
886
|
-
/**
|
|
887
|
-
* For find many calls
|
|
888
|
-
*/
|
|
889
|
-
many: {
|
|
890
|
-
extras: filters?.extras,
|
|
891
|
-
where: sanitizeFilterValue(filters?.where),
|
|
892
|
-
columns: filters?.columns,
|
|
893
|
-
get limit() {
|
|
894
|
-
return limit();
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
},
|
|
898
|
-
/**
|
|
899
|
-
* Query filters for the drizzle SQL API as used in e.g. updates.
|
|
900
|
-
* @example
|
|
901
|
-
*
|
|
902
|
-
* ```ts
|
|
903
|
-
* await db
|
|
904
|
-
* .update(schema.users)
|
|
905
|
-
* .set({
|
|
906
|
-
* name: args.newName,
|
|
907
|
-
* })
|
|
908
|
-
* .where(
|
|
909
|
-
* and(
|
|
910
|
-
* eq(schema.users.id, args.userId),
|
|
911
|
-
* (await ctx.abilities.users.filter("update")).sql.where,
|
|
912
|
-
* ),
|
|
913
|
-
* );
|
|
914
|
-
* ```
|
|
915
|
-
*
|
|
916
|
-
*/
|
|
917
|
-
sql: { get where() {
|
|
918
|
-
return sqlTransformedWhere();
|
|
919
|
-
} }
|
|
920
|
-
};
|
|
921
|
-
else return {
|
|
922
|
-
/**
|
|
923
|
-
* Query filters for the drizzle query API.
|
|
924
|
-
* @example
|
|
925
|
-
* ```ts
|
|
926
|
-
* author: t.relation("author", {
|
|
927
|
-
* query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
|
|
928
|
-
* }),
|
|
929
|
-
* ´´´
|
|
930
|
-
*/
|
|
931
|
-
query: {
|
|
932
|
-
/**
|
|
933
|
-
* For find first calls
|
|
934
|
-
*/
|
|
935
|
-
single: {
|
|
936
|
-
extras: filters?.extras,
|
|
937
|
-
where: sanitizeFilterValue(filters?.where)
|
|
938
|
-
},
|
|
939
|
-
/**
|
|
940
|
-
* For find many calls
|
|
941
|
-
*/
|
|
942
|
-
many: {
|
|
943
|
-
extras: filters?.extras,
|
|
944
|
-
where: sanitizeFilterValue(filters?.where),
|
|
945
|
-
get limit() {
|
|
946
|
-
return limit();
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
},
|
|
950
|
-
/**
|
|
951
|
-
* Query filters for the drizzle SQL API as used in e.g. updates.
|
|
952
|
-
* @example
|
|
953
|
-
*
|
|
954
|
-
* ```ts
|
|
955
|
-
* await db
|
|
956
|
-
* .update(schema.users)
|
|
957
|
-
* .set({
|
|
958
|
-
* name: args.newName,
|
|
959
|
-
* })
|
|
960
|
-
* .where(
|
|
961
|
-
* and(
|
|
962
|
-
* eq(schema.users.id, args.userId),
|
|
963
|
-
* (await ctx.abilities.users.filter("update")).sql.where,
|
|
964
|
-
* ),
|
|
965
|
-
* );
|
|
966
|
-
* ```
|
|
967
|
-
*
|
|
968
|
-
*/
|
|
969
|
-
sql: { get where() {
|
|
970
|
-
return sqlTransformedWhere();
|
|
971
|
-
} }
|
|
972
|
-
};
|
|
973
|
-
};
|
|
974
|
-
const ret = internalTransformer(queryFilters);
|
|
975
|
-
/**
|
|
976
|
-
* Merges the current query filters with the provided filters for this call only
|
|
977
|
-
*/
|
|
978
|
-
function merge(p) {
|
|
979
|
-
const merged = mergeFilters(ret.query.many, p);
|
|
980
|
-
return internalTransformer(merged, p.limit);
|
|
981
|
-
}
|
|
982
|
-
ret.merge = merge;
|
|
983
|
-
return ret;
|
|
984
|
-
}
|
|
985
|
-
return { withContext: (userContext) => {
|
|
986
|
-
const prepare = (action) => {
|
|
987
|
-
const assembleAbilities = async (attributes) => {
|
|
988
|
-
const filters = queryFilters.get(action);
|
|
989
|
-
if (filters === "unrestricted") {
|
|
990
|
-
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
991
|
-
return transformToResponse();
|
|
992
|
-
}
|
|
993
|
-
if (!filters) {
|
|
994
|
-
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
995
|
-
nothingRegisteredWarningLogger(String(tableName), action);
|
|
996
|
-
return transformToResponse(blockEverythingFilter);
|
|
997
|
-
}
|
|
998
|
-
const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
|
|
999
|
-
if (rawResults.includes("allow")) {
|
|
1000
|
-
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1001
|
-
return transformToResponse();
|
|
1002
|
-
}
|
|
1003
|
-
const dynamicResults = rawResults.filter((r) => r !== void 0);
|
|
1004
|
-
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
1005
|
-
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
1006
|
-
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
1007
|
-
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
1008
|
-
if (allQueryFilters.length === 0) {
|
|
1009
|
-
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1010
|
-
return transformToResponse(blockEverythingFilter);
|
|
1011
|
-
}
|
|
1012
|
-
const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
|
|
1013
|
-
return mergeFilters(a, b, "OR");
|
|
1014
|
-
});
|
|
1015
|
-
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
1016
|
-
return transformToResponse(mergedFilters);
|
|
1017
|
-
};
|
|
1018
|
-
const run = async (span) => {
|
|
1019
|
-
const attributes = {
|
|
1020
|
-
[ATTR_TABLE]: String(tableName),
|
|
1021
|
-
[ATTR_ACTION]: action
|
|
1022
|
-
};
|
|
1023
|
-
try {
|
|
1024
|
-
const result = await assembleAbilities(attributes);
|
|
1025
|
-
log?.debug({
|
|
1026
|
-
...attributes,
|
|
1027
|
-
...traceCorrelationFields(telemetryConfig, span)
|
|
1028
|
-
}, "abilities prepared");
|
|
1029
|
-
return result;
|
|
1030
|
-
} catch (error) {
|
|
1031
|
-
recordSpanError(span, error);
|
|
1032
|
-
log?.error({
|
|
1033
|
-
...attributes,
|
|
1034
|
-
...traceCorrelationFields(telemetryConfig, span),
|
|
1035
|
-
...errorLogField(error)
|
|
1036
|
-
}, "abilities failed");
|
|
1037
|
-
throw error;
|
|
1038
|
-
} finally {
|
|
1039
|
-
span?.setAttributes(attributes);
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
|
|
1043
|
-
try {
|
|
1044
|
-
return await run(span);
|
|
1045
|
-
} finally {
|
|
1046
|
-
span.end();
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
return run();
|
|
1050
|
-
};
|
|
1051
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1052
|
-
return { filter: (action) => {
|
|
1053
|
-
let prepared = cache.get(action);
|
|
1054
|
-
if (!prepared) {
|
|
1055
|
-
prepared = guardAgainstMissingAwait(prepare(action));
|
|
1056
|
-
cache.set(action, prepared);
|
|
1057
|
-
}
|
|
1058
|
-
return prepared;
|
|
1059
|
-
} };
|
|
1060
|
-
} };
|
|
1061
|
-
};
|
|
1062
|
-
const abilitiesPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createFilterForTable(tableName)]));
|
|
1276
|
+
const abilitiesPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createTableAbilities(settings, tableName, buildersPerTable[tableName]._.queryFilters)]));
|
|
1063
1277
|
hasBeenBuilt = true;
|
|
1064
1278
|
return (ctx) => {
|
|
1065
|
-
|
|
1279
|
+
const state = createRequestState();
|
|
1280
|
+
const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
|
|
1281
|
+
attachRequestState(abilities, state);
|
|
1282
|
+
return abilities;
|
|
1066
1283
|
};
|
|
1067
1284
|
}
|
|
1068
1285
|
}
|
|
@@ -1886,7 +2103,8 @@ async function initSearchIfApplicable(input) {
|
|
|
1886
2103
|
*/
|
|
1887
2104
|
function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
1888
2105
|
if (search?.enabled && args.search && args.search.length > 0) {
|
|
1889
|
-
const
|
|
2106
|
+
const columnMask = abilities[columnMaskKey];
|
|
2107
|
+
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()));
|
|
1890
2108
|
const searchParam = drizzle_orm.sql`${args.search}`;
|
|
1891
2109
|
args.extras = { search_distance: (table) => drizzle_orm.sql`${drizzle_orm.sql.join(columnsToSearch.map(([key]) => {
|
|
1892
2110
|
return drizzle_orm.sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
|
|
@@ -1944,14 +2162,15 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
1944
2162
|
db,
|
|
1945
2163
|
table
|
|
1946
2164
|
});
|
|
2165
|
+
abilityBuilder._.registerReadAction(table, readAction);
|
|
1947
2166
|
if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
|
|
1948
|
-
const
|
|
2167
|
+
const primaryKeyName = Object.keys(tableSchema.primaryKey)[0];
|
|
1949
2168
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
1950
2169
|
return schemaBuilder.drizzleObject(table, {
|
|
1951
2170
|
name: refName ?? (0, es_toolkit.capitalize)(String(table)),
|
|
1952
2171
|
subscribe: (subscriptions, element, _context) => {
|
|
1953
|
-
if (!
|
|
1954
|
-
const primaryKeyValue = element[
|
|
2172
|
+
if (!primaryKeyName) return;
|
|
2173
|
+
const primaryKeyValue = element[primaryKeyName];
|
|
1955
2174
|
if (!primaryKeyValue) {
|
|
1956
2175
|
warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
|
|
1957
2176
|
return;
|
|
@@ -1966,6 +2185,16 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
1966
2185
|
table,
|
|
1967
2186
|
action: readAction
|
|
1968
2187
|
}),
|
|
2188
|
+
maskColumns: ({ context, entities, action }) => abilityBuilder._.maskColumns({
|
|
2189
|
+
table,
|
|
2190
|
+
action: action ?? readAction,
|
|
2191
|
+
abilities: context.abilities,
|
|
2192
|
+
entities
|
|
2193
|
+
}),
|
|
2194
|
+
maskPrimaryKeys: {
|
|
2195
|
+
fields: Object.keys(tableSchema.primaryKey),
|
|
2196
|
+
isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
|
|
2197
|
+
},
|
|
1969
2198
|
fields: (t) => {
|
|
1970
2199
|
const columns = tableSchema.columns;
|
|
1971
2200
|
const configMap = /* @__PURE__ */ new Map();
|
|
@@ -2201,7 +2430,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2201
2430
|
db,
|
|
2202
2431
|
table
|
|
2203
2432
|
});
|
|
2204
|
-
const
|
|
2433
|
+
const primaryKeyName = Object.keys(tableSchema.primaryKey)[0];
|
|
2205
2434
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
2206
2435
|
const pluralName = pluralize.default.plural(String(table));
|
|
2207
2436
|
const singularName = pluralize.default.singular(String(table));
|
|
@@ -2229,6 +2458,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2229
2458
|
type: [table],
|
|
2230
2459
|
nullable: false,
|
|
2231
2460
|
smartSubscription: true,
|
|
2461
|
+
maskColumnsAction: listAction,
|
|
2232
2462
|
description: `List all ${pluralName}`,
|
|
2233
2463
|
subscribe: (subscriptions, _root, _args, _ctx, _info) => {
|
|
2234
2464
|
registerOnInstance({
|
|
@@ -2276,11 +2506,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2276
2506
|
type: table,
|
|
2277
2507
|
nullable: false,
|
|
2278
2508
|
smartSubscription: true,
|
|
2509
|
+
maskColumnsAction: readAction,
|
|
2279
2510
|
description: `Get a single ${singularName} by ID`,
|
|
2280
2511
|
args: { id: t.arg.id({ required: true }) },
|
|
2281
2512
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
2282
2513
|
deepSetProto(args);
|
|
2283
|
-
const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [
|
|
2514
|
+
const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [primaryKeyName]: args.id } }).query.single;
|
|
2284
2515
|
const queryArgs = query(filter);
|
|
2285
2516
|
if (filter.columns) queryArgs.columns = filter.columns;
|
|
2286
2517
|
return findFirst ? findFirst({
|
|
@@ -2548,6 +2779,108 @@ function implementDefaultWhereInputArgs(schemaBuilder) {
|
|
|
2548
2779
|
}) });
|
|
2549
2780
|
}
|
|
2550
2781
|
//#endregion
|
|
2782
|
+
//#region lib/helpers/objectTypeOptions.ts
|
|
2783
|
+
function objectTypeOptionsOfField(buildCache, fieldConfig) {
|
|
2784
|
+
const type = fieldConfig.type;
|
|
2785
|
+
const ref = type.kind === "List" ? type.type.kind === "Object" ? type.type.ref : void 0 : type.kind === "Object" ? type.ref : void 0;
|
|
2786
|
+
if (!ref) return void 0;
|
|
2787
|
+
return buildCache.getTypeConfig(ref, "Object").pothosOptions;
|
|
2788
|
+
}
|
|
2789
|
+
//#endregion
|
|
2790
|
+
//#region lib/helpers/registerPlugin.ts
|
|
2791
|
+
const registered = /* @__PURE__ */ new Set();
|
|
2792
|
+
function registerPluginOnce(name, plugin) {
|
|
2793
|
+
if (registered.has(name)) return;
|
|
2794
|
+
_pothos_core.default.allowPluginReRegistration = true;
|
|
2795
|
+
_pothos_core.default.registerPlugin(name, plugin);
|
|
2796
|
+
registered.add(name);
|
|
2797
|
+
}
|
|
2798
|
+
//#endregion
|
|
2799
|
+
//#region lib/columnMaskPlugin/maskTypes.ts
|
|
2800
|
+
const maskColumnsKey = "maskColumns";
|
|
2801
|
+
const maskColumnsActionKey = "maskColumnsAction";
|
|
2802
|
+
const maskPrimaryKeysKey = "maskPrimaryKeys";
|
|
2803
|
+
const columnMaskPluginName = "ColumnMaskPlugin";
|
|
2804
|
+
//#endregion
|
|
2805
|
+
//#region lib/columnMaskPlugin/columnMaskPlugin.ts
|
|
2806
|
+
var ColumnMaskPlugin = class extends _pothos_core.BasePlugin {
|
|
2807
|
+
wrapResolve(resolver, fieldConfig) {
|
|
2808
|
+
return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
|
|
2809
|
+
}
|
|
2810
|
+
maskPrimaryKey(resolver, fieldConfig) {
|
|
2811
|
+
const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
|
|
2812
|
+
if (!primaryKeys?.fields.includes(fieldConfig.name)) return resolver;
|
|
2813
|
+
return (parent, args, context, info) => primaryKeys.isHidden(context, parent) ? null : resolver(parent, args, context, info);
|
|
2814
|
+
}
|
|
2815
|
+
maskResult(resolver, fieldConfig) {
|
|
2816
|
+
const maskColumns = objectTypeOptionsOfField(this.buildCache, fieldConfig)?.[maskColumnsKey];
|
|
2817
|
+
if (!maskColumns) return resolver;
|
|
2818
|
+
const action = fieldConfig.pothosOptions[maskColumnsActionKey];
|
|
2819
|
+
const mask = (value, context) => {
|
|
2820
|
+
if (value == null) return value;
|
|
2821
|
+
if (Array.isArray(value) && value.some(_pothos_core.isThenable)) return Promise.all(value).then((rows) => mask(rows, context));
|
|
2822
|
+
const pending = maskColumns({
|
|
2823
|
+
context,
|
|
2824
|
+
entities: Array.isArray(value) ? value : [value],
|
|
2825
|
+
action
|
|
2826
|
+
});
|
|
2827
|
+
return pending ? pending.then(() => value) : value;
|
|
2828
|
+
};
|
|
2829
|
+
return (parent, args, context, info) => {
|
|
2830
|
+
const resolved = resolver(parent, args, context, info);
|
|
2831
|
+
return (0, _pothos_core.isThenable)(resolved) ? resolved.then((value) => mask(value, context)) : mask(resolved, context);
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
function registerColumnMaskPlugin() {
|
|
2836
|
+
registerPluginOnce(columnMaskPluginName, ColumnMaskPlugin);
|
|
2837
|
+
}
|
|
2838
|
+
//#endregion
|
|
2839
|
+
//#region lib/helpers/abilityAwareClient.ts
|
|
2840
|
+
function createAbilityAwareClient({ db, abilityBuilder }) {
|
|
2841
|
+
return (context) => {
|
|
2842
|
+
const apis = /* @__PURE__ */ new Map();
|
|
2843
|
+
const query = new Proxy(db.query, { get(target, table, receiver) {
|
|
2844
|
+
const api = Reflect.get(target, table, receiver);
|
|
2845
|
+
if (typeof table !== "string" || !context.abilities?.[table]) return api;
|
|
2846
|
+
let wrapped = apis.get(table);
|
|
2847
|
+
if (!wrapped) {
|
|
2848
|
+
wrapped = Object.create(api, { findMany: { value: async (config = {}) => {
|
|
2849
|
+
const masked = abilityBuilder._.maskedActions(context.abilities, table);
|
|
2850
|
+
const actions = masked.size ? [...masked] : [abilityBuilder._.readActionOf(table)];
|
|
2851
|
+
return (await Promise.all(actions.map(async (action) => {
|
|
2852
|
+
const ability = (await context.abilities[table].filter(action)).query.many;
|
|
2853
|
+
const { where, extras } = mergeFilters({
|
|
2854
|
+
where: config.where,
|
|
2855
|
+
extras: config.extras
|
|
2856
|
+
}, {
|
|
2857
|
+
where: ability.where,
|
|
2858
|
+
extras: ability.extras
|
|
2859
|
+
});
|
|
2860
|
+
const rows = await api.findMany({
|
|
2861
|
+
...config,
|
|
2862
|
+
where,
|
|
2863
|
+
extras
|
|
2864
|
+
});
|
|
2865
|
+
await abilityBuilder._.maskColumns({
|
|
2866
|
+
table,
|
|
2867
|
+
action,
|
|
2868
|
+
abilities: context.abilities,
|
|
2869
|
+
entities: rows
|
|
2870
|
+
});
|
|
2871
|
+
return rows;
|
|
2872
|
+
}))).flat();
|
|
2873
|
+
} } });
|
|
2874
|
+
apis.set(table, wrapped);
|
|
2875
|
+
}
|
|
2876
|
+
return wrapped;
|
|
2877
|
+
} });
|
|
2878
|
+
return new Proxy(db, { get(target, prop, receiver) {
|
|
2879
|
+
return prop === "query" ? query : Reflect.get(target, prop, receiver);
|
|
2880
|
+
} });
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
//#endregion
|
|
2551
2884
|
//#region lib/runtimeFiltersPlugin/filterTypes.ts
|
|
2552
2885
|
const pluginName = "RuntimeFiltersPlugin";
|
|
2553
2886
|
//#endregion
|
|
@@ -2558,14 +2891,9 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2558
2891
|
tracerEnabled;
|
|
2559
2892
|
logger;
|
|
2560
2893
|
telemetryConfig = {};
|
|
2561
|
-
filterLoaders = /* @__PURE__ */ new WeakMap();
|
|
2562
2894
|
getLoader(context, filter) {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
perContext = /* @__PURE__ */ new Map();
|
|
2566
|
-
this.filterLoaders.set(context, perContext);
|
|
2567
|
-
}
|
|
2568
|
-
let loader = perContext.get(filter);
|
|
2895
|
+
const loaders = requestStateOf(context.abilities)?.filterLoaders ?? /* @__PURE__ */ new Map();
|
|
2896
|
+
let loader = loaders.get(filter);
|
|
2569
2897
|
if (!loader) {
|
|
2570
2898
|
const prefetchPromise = filter.prefetch ? filter.prefetch({ context }) : void 0;
|
|
2571
2899
|
loader = new dataloader.default(async (entities) => {
|
|
@@ -2578,7 +2906,7 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2578
2906
|
const allowedSet = new Set(allowed);
|
|
2579
2907
|
return entities.map((entity) => allowedSet.has(entity) ? entity : null);
|
|
2580
2908
|
}, { cache: false });
|
|
2581
|
-
|
|
2909
|
+
loaders.set(filter, loader);
|
|
2582
2910
|
}
|
|
2583
2911
|
return loader;
|
|
2584
2912
|
}
|
|
@@ -2593,14 +2921,11 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2593
2921
|
return typeConfig;
|
|
2594
2922
|
}
|
|
2595
2923
|
wrapResolve(resolver, fieldConfig) {
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
else if (fieldType.kind === "Object") filters = fieldType.ref.currentConfig.pothosOptions[applyFiltersKey];
|
|
2601
|
-
if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
|
|
2924
|
+
const filters = objectTypeOptionsOfField(this.buildCache, fieldConfig)?.[applyFiltersKey];
|
|
2925
|
+
if (!filters) return resolver;
|
|
2926
|
+
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2927
|
+
const applyFilters = async (parent, args, context, info) => {
|
|
2602
2928
|
const runFilters = async (span) => {
|
|
2603
|
-
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2604
2929
|
span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
|
|
2605
2930
|
const loaders = allFilters.map((filter) => this.getLoader(context, filter));
|
|
2606
2931
|
let resolved;
|
|
@@ -2655,15 +2980,11 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2655
2980
|
throw error;
|
|
2656
2981
|
}
|
|
2657
2982
|
};
|
|
2983
|
+
return (parent, args, context, info) => allFilters.length === 0 ? resolver(parent, args, context, info) : applyFilters(parent, args, context, info);
|
|
2658
2984
|
}
|
|
2659
2985
|
};
|
|
2660
|
-
let registered = false;
|
|
2661
2986
|
function registerRuntimeFiltersPlugin() {
|
|
2662
|
-
|
|
2663
|
-
_pothos_core.default.allowPluginReRegistration = true;
|
|
2664
|
-
_pothos_core.default.registerPlugin(pluginName, RuntimeFiltersPlugin);
|
|
2665
|
-
registered = true;
|
|
2666
|
-
}
|
|
2987
|
+
registerPluginOnce(pluginName, RuntimeFiltersPlugin);
|
|
2667
2988
|
}
|
|
2668
2989
|
//#endregion
|
|
2669
2990
|
//#region lib/scalars/addressValidation.ts
|
|
@@ -2794,9 +3115,13 @@ function implementDefaultAddressTypes(schemaBuilder) {
|
|
|
2794
3115
|
//#endregion
|
|
2795
3116
|
//#region lib/scalars/emailAddress.ts
|
|
2796
3117
|
/**
|
|
2797
|
-
* Validates an email address using graphql-scalars' own format check, then lowercases it
|
|
3118
|
+
* Validates an email address using graphql-scalars' own format check, then lowercases it, exactly
|
|
3119
|
+
* as the `EmailAddress` scalar does. Exported for values that do not arrive as GraphQL input,
|
|
3120
|
+
* such as OIDC claims.
|
|
3121
|
+
*
|
|
3122
|
+
* @throws RumbleErrorSafe if the value is not a valid email address
|
|
2798
3123
|
*/
|
|
2799
|
-
const
|
|
3124
|
+
const normalizeEmailAddress = (value) => {
|
|
2800
3125
|
try {
|
|
2801
3126
|
return graphql_scalars.EmailAddressResolver.parseValue(value).toLowerCase();
|
|
2802
3127
|
} catch (error) {
|
|
@@ -2807,23 +3132,28 @@ const EmailAddressResolver = new graphql.GraphQLScalarType({
|
|
|
2807
3132
|
name: "EmailAddress",
|
|
2808
3133
|
description: `${graphql_scalars.EmailAddressResolver.description} Lowercased.`,
|
|
2809
3134
|
specifiedByURL: graphql_scalars.EmailAddressResolver.specifiedByURL,
|
|
2810
|
-
serialize:
|
|
2811
|
-
parseValue:
|
|
3135
|
+
serialize: normalizeEmailAddress,
|
|
3136
|
+
parseValue: normalizeEmailAddress,
|
|
2812
3137
|
parseLiteral(ast) {
|
|
2813
3138
|
if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as email addresses but got a: ${ast.kind}`);
|
|
2814
|
-
return
|
|
3139
|
+
return normalizeEmailAddress(ast.value);
|
|
2815
3140
|
}
|
|
2816
3141
|
});
|
|
2817
3142
|
//#endregion
|
|
2818
3143
|
//#region lib/scalars/personName.ts
|
|
2819
3144
|
const MAX_LENGTH = 200;
|
|
2820
3145
|
const NAME_REGEX = /^[\p{L}\p{M}]+(?:(?:[ '-]|\.\x20?)[\p{L}\p{M}]+)*\.?$/u;
|
|
3146
|
+
const APOSTROPHE_VARIANTS = /[\u2018\u2019\u02BC\u00B4\u0060]/gu;
|
|
3147
|
+
const HYPHEN_VARIANTS = /[\u2010-\u2015\u2212]/gu;
|
|
2821
3148
|
/**
|
|
2822
|
-
* Normalizes and validates a person's name.
|
|
3149
|
+
* Normalizes and validates a person's name, exactly as the `PersonName` scalar does. Exported
|
|
3150
|
+
* for values that do not arrive as GraphQL input, such as OIDC claims.
|
|
3151
|
+
*
|
|
3152
|
+
* @throws RumbleErrorSafe if the value is not a valid name
|
|
2823
3153
|
*/
|
|
2824
|
-
const
|
|
3154
|
+
const normalizePersonName = (value) => {
|
|
2825
3155
|
if (typeof value !== "string") throw new RumbleErrorSafe(`Name value is not a string: ${value}`);
|
|
2826
|
-
const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\p{Zs}+/gu, " ").trim().replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
|
|
3156
|
+
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();
|
|
2827
3157
|
if (normalized.length === 0) throw new RumbleErrorSafe("Name must not be empty.");
|
|
2828
3158
|
if (normalized.length > MAX_LENGTH) throw new RumbleErrorSafe(`Name must not be longer than ${MAX_LENGTH} characters.`);
|
|
2829
3159
|
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.`);
|
|
@@ -2831,12 +3161,12 @@ const normalize = (value) => {
|
|
|
2831
3161
|
};
|
|
2832
3162
|
const PersonNameResolver = new graphql.GraphQLScalarType({
|
|
2833
3163
|
name: "PersonName",
|
|
2834
|
-
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.",
|
|
2835
|
-
serialize:
|
|
2836
|
-
parseValue:
|
|
3164
|
+
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.",
|
|
3165
|
+
serialize: normalizePersonName,
|
|
3166
|
+
parseValue: normalizePersonName,
|
|
2837
3167
|
parseLiteral(ast) {
|
|
2838
3168
|
if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as names but got a: ${ast.kind}`);
|
|
2839
|
-
return
|
|
3169
|
+
return normalizePersonName(ast.value);
|
|
2840
3170
|
}
|
|
2841
3171
|
});
|
|
2842
3172
|
//#endregion
|
|
@@ -2844,9 +3174,12 @@ const PersonNameResolver = new graphql.GraphQLScalarType({
|
|
|
2844
3174
|
/**
|
|
2845
3175
|
* Parses and validates a phone number using libphonenumber-js (Google's phone number metadata),
|
|
2846
3176
|
* which checks real per-country validity (length, area code plausibility, ...) rather than just
|
|
2847
|
-
* the E.164 shape.
|
|
3177
|
+
* the E.164 shape. This is what the `PhoneNumber` scalar does with its input; exported for values
|
|
3178
|
+
* that do not arrive as GraphQL input. `.number` of the result is the E.164 form.
|
|
3179
|
+
*
|
|
3180
|
+
* @throws RumbleErrorSafe if the value is not a valid phone number
|
|
2848
3181
|
*/
|
|
2849
|
-
const
|
|
3182
|
+
const parsePhoneNumber = (value) => {
|
|
2850
3183
|
if (typeof value !== "string") throw new RumbleErrorSafe(`Phone number value is not a string: ${value}`);
|
|
2851
3184
|
const phoneNumber = (0, libphonenumber_js.parsePhoneNumberFromString)(value);
|
|
2852
3185
|
if (!phoneNumber?.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${value}. Please provide a valid phone number including its country calling code, e.g. +4915123456789.`);
|
|
@@ -2862,26 +3195,28 @@ const serialize = (value) => {
|
|
|
2862
3195
|
if (!phoneNumber.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${phoneNumber.number}`);
|
|
2863
3196
|
return phoneNumber.number;
|
|
2864
3197
|
}
|
|
2865
|
-
return
|
|
3198
|
+
return parsePhoneNumber(value).number;
|
|
2866
3199
|
};
|
|
2867
3200
|
const PhoneNumberResolver = new graphql.GraphQLScalarType({
|
|
2868
3201
|
name: "PhoneNumber",
|
|
2869
3202
|
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.",
|
|
2870
3203
|
serialize,
|
|
2871
|
-
parseValue:
|
|
3204
|
+
parseValue: parsePhoneNumber,
|
|
2872
3205
|
parseLiteral(ast) {
|
|
2873
3206
|
if (ast.kind !== graphql.Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as phone numbers but got a: ${ast.kind}`);
|
|
2874
|
-
return
|
|
3207
|
+
return parsePhoneNumber(ast.value);
|
|
2875
3208
|
}
|
|
2876
3209
|
});
|
|
2877
3210
|
//#endregion
|
|
2878
3211
|
//#region lib/schemaBuilder.ts
|
|
2879
|
-
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
|
|
3212
|
+
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation, abilityBuilder }) => {
|
|
2880
3213
|
const createSpan = otel?.enabled && otel.tracer ? (0, _pothos_tracing_opentelemetry.createOpenTelemetryWrapper)(otel.tracer, otel.options) : void 0;
|
|
2881
3214
|
registerRuntimeFiltersPlugin();
|
|
3215
|
+
registerColumnMaskPlugin();
|
|
2882
3216
|
const schemaBuilder = new _pothos_core.default({
|
|
2883
3217
|
...pothosConfig,
|
|
2884
3218
|
plugins: [
|
|
3219
|
+
columnMaskPluginName,
|
|
2885
3220
|
pluginName,
|
|
2886
3221
|
_pothos_plugin_drizzle.default,
|
|
2887
3222
|
_pothos_plugin_smart_subscriptions.default,
|
|
@@ -2890,13 +3225,19 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validatio
|
|
|
2890
3225
|
...pothosConfig?.plugins ?? []
|
|
2891
3226
|
],
|
|
2892
3227
|
drizzle: {
|
|
2893
|
-
client:
|
|
3228
|
+
client: createAbilityAwareClient({
|
|
3229
|
+
db,
|
|
3230
|
+
abilityBuilder
|
|
3231
|
+
}),
|
|
2894
3232
|
relations: db._.relations,
|
|
2895
3233
|
getTableConfig(table) {
|
|
2896
|
-
|
|
3234
|
+
if ((0, drizzle_orm.is)(table, drizzle_orm_pg_core.PgTable)) return (0, drizzle_orm_pg_core.getTableConfig)(table);
|
|
3235
|
+
if ((0, drizzle_orm.is)(table, drizzle_orm_mysql_core.MySqlTable)) return (0, drizzle_orm_mysql_core.getTableConfig)(table);
|
|
3236
|
+
if ((0, drizzle_orm.is)(table, drizzle_orm_sqlite_core.SQLiteTable)) return (0, drizzle_orm_sqlite_core.getTableConfig)(table);
|
|
2897
3237
|
return {
|
|
2898
|
-
columns,
|
|
2899
|
-
primaryKeys:
|
|
3238
|
+
columns: (0, drizzle_orm.isTable)(table) ? Object.values((0, drizzle_orm.getTableColumns)(table)) : [],
|
|
3239
|
+
primaryKeys: [],
|
|
3240
|
+
uniqueConstraints: []
|
|
2900
3241
|
};
|
|
2901
3242
|
}
|
|
2902
3243
|
},
|
|
@@ -3015,7 +3356,8 @@ export const r = rumble({
|
|
|
3015
3356
|
const { makePubSubInstance, pubsub } = createPubSubInstance({ ...rumbleInput });
|
|
3016
3357
|
const { schemaBuilder } = createSchemaBuilder({
|
|
3017
3358
|
...rumbleInput,
|
|
3018
|
-
pubsub
|
|
3359
|
+
pubsub,
|
|
3360
|
+
abilityBuilder
|
|
3019
3361
|
});
|
|
3020
3362
|
const enum_ = createEnumImplementer({
|
|
3021
3363
|
...rumbleInput,
|
|
@@ -3284,6 +3626,9 @@ exports.RumbleErrorSafe = RumbleErrorSafe;
|
|
|
3284
3626
|
exports.assertFindFirstExists = assertFindFirstExists;
|
|
3285
3627
|
exports.assertFirstEntryExists = assertFirstEntryExists;
|
|
3286
3628
|
exports.mapNullFieldsToUndefined = mapNullFieldsToUndefined;
|
|
3629
|
+
exports.normalizeEmailAddress = normalizeEmailAddress;
|
|
3630
|
+
exports.normalizePersonName = normalizePersonName;
|
|
3631
|
+
exports.parsePhoneNumber = parsePhoneNumber;
|
|
3287
3632
|
exports.rumble = rumble;
|
|
3288
3633
|
|
|
3289
3634
|
//# sourceMappingURL=index.cjs.map
|