@m1212e/rumble 0.24.2 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.24.2";
219
+ var version = "0.25.1";
220
220
  //#endregion
221
- //#region lib/helpers/errorLogging.ts
222
- function hasMessage(error) {
223
- return typeof error === "object" && error !== null && "message" in error;
224
- }
225
- function toLoggedException(error) {
226
- if (hasMessage(error)) {
227
- const gqlError = error;
228
- return {
229
- "exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
230
- "exception.message": String(gqlError.message),
231
- ...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
232
- ...gqlError.path ? { path: gqlError.path } : {},
233
- ...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
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
- "exception.type": "UnknownError",
238
- "exception.message": String(error)
249
+ byTsName,
250
+ byDbName,
251
+ byTableRef
239
252
  };
240
253
  }
241
- /**
242
- * Every erroneous rumble log carries the same `errors` array shape,
243
- * whether it originates from a thrown exception (single-element array)
244
- * or a GraphQL `ExecutionResult.errors` list (multi-element array).
245
- */
246
- function errorsLogField(errors) {
247
- return { errors: errors.map(toLoggedException) };
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 errorLogField(error) {
250
- return errorsLogField([error]);
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: filterAWhere && filterBWhere ? mode === "OR" ? { OR: [filterAWhere, filterBWhere] } : { AND: [filterAWhere, filterBWhere] } : filterAWhere ?? filterBWhere,
270
- columns: filterA?.columns || filterB?.columns ? new Set([Object.entries(filterA?.columns ?? {}), Object.entries(filterB?.columns ?? {})].flat().filter(([, v]) => v === true).map(([k]) => k)).entries().reduce((acc, [key]) => {
271
- acc[key] = true;
272
- return acc;
273
- }, {}) : void 0,
274
- extras: filterA?.extras || filterB?.extras ? (0, es_toolkit.toMerged)(filterA?.extras ?? {}, filterB?.extras ?? {}) : void 0,
275
- orderBy: filterA?.orderBy || filterB?.orderBy ? (0, es_toolkit.toMerged)(filterA?.orderBy ?? {}, filterB?.orderBy ?? {}) : void 0,
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
- const makeNothingRegisteredWarner = (config, logger) => (0, es_toolkit.debounce)((model, action) => {
711
- const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
712
- if (logger) logger.warn({
713
- [ATTR_TABLE]: model,
714
- [ATTR_ACTION]: action,
715
- ...traceCorrelationFields(config)
716
- }, msg);
717
- else console.warn(msg);
718
- }, 1e3);
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 nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
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 createBuilderForTable = () => {
732
- const queryFilters = /* @__PURE__ */ new Map();
733
- const runtimeFilters = /* @__PURE__ */ new Map();
734
- for (const action of actions) if (!runtimeFilters.has(action)) runtimeFilters.set(action, []);
735
- return {
736
- /**
737
- * Allows to perform a specific action on a specific entity
738
- */
739
- allow: (action) => {
740
- if (hasBeenBuilt) 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.");
741
- const actions = Array.isArray(action) ? action : [action];
742
- const setUnrestrictedByThisCall = /* @__PURE__ */ new Set();
743
- for (const action of actions) {
744
- let filters = queryFilters.get(action);
745
- if (!filters) {
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 createFilterForTable = (tableName) => {
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
- return Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
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 columnsToSearch = (abilities.query.many.columns ? Object.entries(tableSchema.columns).filter(([key]) => abilities.query.many.columns[key]) : Object.entries(tableSchema.columns)).filter(([_key, col]) => isStringLikeSQLTypeString(col.getSQLType()) || isIDLikeSQLTypeString(col.getSQLType()));
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 primaryKey = Object.values(tableSchema.primaryKey)[0];
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 (!primaryKey) return;
1954
- const primaryKeyValue = element[primaryKey.name];
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 primaryKeyField = Object.values(tableSchema.primaryKey)[0];
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: { [primaryKeyField.name]: args.id } }).query.single;
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
- let perContext = this.filterLoaders.get(context);
2564
- if (!perContext) {
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
- perContext.set(filter, loader);
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
- return async (parent, args, context, info) => {
2597
- let filters;
2598
- const fieldType = fieldConfig?.type;
2599
- if (fieldType.kind === "List") filters = fieldType.type?.ref.currentConfig.pothosOptions[applyFiltersKey];
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
- if (!registered) {
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
@@ -2876,12 +3197,14 @@ const PhoneNumberResolver = new graphql.GraphQLScalarType({
2876
3197
  });
2877
3198
  //#endregion
2878
3199
  //#region lib/schemaBuilder.ts
2879
- const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
3200
+ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation, abilityBuilder }) => {
2880
3201
  const createSpan = otel?.enabled && otel.tracer ? (0, _pothos_tracing_opentelemetry.createOpenTelemetryWrapper)(otel.tracer, otel.options) : void 0;
2881
3202
  registerRuntimeFiltersPlugin();
3203
+ registerColumnMaskPlugin();
2882
3204
  const schemaBuilder = new _pothos_core.default({
2883
3205
  ...pothosConfig,
2884
3206
  plugins: [
3207
+ columnMaskPluginName,
2885
3208
  pluginName,
2886
3209
  _pothos_plugin_drizzle.default,
2887
3210
  _pothos_plugin_smart_subscriptions.default,
@@ -2890,13 +3213,19 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validatio
2890
3213
  ...pothosConfig?.plugins ?? []
2891
3214
  ],
2892
3215
  drizzle: {
2893
- client: db,
3216
+ client: createAbilityAwareClient({
3217
+ db,
3218
+ abilityBuilder
3219
+ }),
2894
3220
  relations: db._.relations,
2895
3221
  getTableConfig(table) {
2896
- const columns = (0, drizzle_orm.isTable)(table) ? Object.values((0, drizzle_orm.getTableColumns)(table)) : [];
3222
+ if ((0, drizzle_orm.is)(table, drizzle_orm_pg_core.PgTable)) return (0, drizzle_orm_pg_core.getTableConfig)(table);
3223
+ if ((0, drizzle_orm.is)(table, drizzle_orm_mysql_core.MySqlTable)) return (0, drizzle_orm_mysql_core.getTableConfig)(table);
3224
+ if ((0, drizzle_orm.is)(table, drizzle_orm_sqlite_core.SQLiteTable)) return (0, drizzle_orm_sqlite_core.getTableConfig)(table);
2897
3225
  return {
2898
- columns,
2899
- primaryKeys: columns.filter((v) => v.primary)
3226
+ columns: (0, drizzle_orm.isTable)(table) ? Object.values((0, drizzle_orm.getTableColumns)(table)) : [],
3227
+ primaryKeys: [],
3228
+ uniqueConstraints: []
2900
3229
  };
2901
3230
  }
2902
3231
  },
@@ -3015,7 +3344,8 @@ export const r = rumble({
3015
3344
  const { makePubSubInstance, pubsub } = createPubSubInstance({ ...rumbleInput });
3016
3345
  const { schemaBuilder } = createSchemaBuilder({
3017
3346
  ...rumbleInput,
3018
- pubsub
3347
+ pubsub,
3348
+ abilityBuilder
3019
3349
  });
3020
3350
  const enum_ = createEnumImplementer({
3021
3351
  ...rumbleInput,