@m1212e/rumble 0.25.3 → 0.26.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
@@ -22,16 +22,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
  //#endregion
24
24
  const require_lazy = require("./lazy-CAN0oaXD.cjs");
25
- const require_generate = require("./generate-Ci6AXNnn.cjs");
25
+ const require_generate = require("./generate-D0CKBths.cjs");
26
+ let drizzle_orm = require("drizzle-orm");
26
27
  let graphql = require("graphql");
27
- let _escape_tech_graphql_armor = require("@escape.tech/graphql-armor");
28
- let _graphql_yoga_plugin_disable_introspection = require("@graphql-yoga/plugin-disable-introspection");
29
- let _opentelemetry_api = require("@opentelemetry/api");
30
28
  let es_toolkit = require("es-toolkit");
31
- let graphql_yoga = require("graphql-yoga");
32
- let drizzle_orm = require("drizzle-orm");
29
+ let _opentelemetry_api = require("@opentelemetry/api");
33
30
  let _opentelemetry_semantic_conventions_incubating = require("@opentelemetry/semantic-conventions/incubating");
34
31
  let _pothos_tracing_opentelemetry = require("@pothos/tracing-opentelemetry");
32
+ let _escape_tech_graphql_armor = require("@escape.tech/graphql-armor");
33
+ let _graphql_yoga_plugin_disable_introspection = require("@graphql-yoga/plugin-disable-introspection");
34
+ let graphql_yoga = require("graphql-yoga");
35
35
  let drizzle_orm_casing = require("drizzle-orm/casing");
36
36
  let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
37
37
  let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
@@ -72,152 +72,6 @@ var RumbleError = class extends Error {
72
72
  */
73
73
  var RumbleErrorSafe = class extends graphql.GraphQLError {};
74
74
  //#endregion
75
- //#region lib/helpers/asserts.ts
76
- /**
77
- *
78
- * Helper function to map a drizzle findFirst query result,
79
- * which may be optional, to a correct drizzle type.
80
- *
81
- * @throws RumbleError
82
- *
83
- * @example
84
- *
85
- * ```ts
86
- * schemaBuilder.queryFields((t) => {
87
- return {
88
- findFirstUser: t.drizzleField({
89
- type: UserRef,
90
- resolve: (query, root, args, ctx, info) => {
91
- return (
92
- db.query.users
93
- .findFirst({
94
- ...query,
95
- where: (await ctx.abilities.users.filter("read")).single.where,
96
- })
97
- // note that we need to manually raise an error if the value is not found
98
- .then(assertFindFirstExists)
99
- );
100
- },
101
- }),
102
- };
103
- });
104
- * ```
105
- */
106
- const assertFindFirstExists = (value) => {
107
- if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
108
- return value;
109
- };
110
- /**
111
- *
112
- * Helper function to map a drizzle findFirst query result,
113
- * which may be optional, to a correct drizzle type.
114
- *
115
- * @throws RumbleError
116
- *
117
- * @example
118
- *
119
- * ```ts
120
- schemaBuilder.mutationFields((t) => {
121
- return {
122
- updateUsername: t.drizzleField({
123
- type: UserRef,
124
- args: {
125
- userId: t.arg.int({ required: true }),
126
- newName: t.arg.string({ required: true }),
127
- },
128
- resolve: (query, root, args, ctx, info) => {
129
- return db
130
- .update(schema.users)
131
- .set({
132
- name: args.newName,
133
- })
134
- .where(
135
- and(
136
- eq(schema.users.id, args.userId),
137
- (await ctx.abilities.users.filter("update")).single.where
138
- )
139
- )
140
- .returning({ id: schema.users.id, name: schema.users.name })
141
- // note that we need to manually raise an error if the value is not found
142
- .then(assertFirstEntryExists);
143
- },
144
- }),
145
- };
146
- });
147
- * ```
148
- */
149
- const assertFirstEntryExists = (value) => {
150
- const v = value.at(0);
151
- if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
152
- return v;
153
- };
154
- //#endregion
155
- //#region lib/helpers/mapNullFieldsToUndefined.ts
156
- /**
157
- * Helper to map null fields to undefined
158
- * @param obj The object to map
159
- * @returns The mapped object with all fields of 'null' transformed to 'undefined'
160
- *
161
- * This becomes useful for update mutations where you do not want to pass null (unset value in db)
162
- * but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
163
- * @example
164
- * ```ts
165
- * updateUser: t.drizzleField({
166
- type: User,
167
- args: {
168
- id: t.arg.string({ required: true }),
169
- email: t.arg.string(),
170
- lastName: t.arg.string(),
171
- firstName: t.arg.string(),
172
- },
173
- resolve: async (query, root, args, ctx, info) => {
174
- const mappedArgs = mapNullFieldsToUndefined(args);
175
- const user = await db.transaction(async (tx) => {
176
- const user = await tx
177
- .update(schema.user)
178
- .set({
179
-
180
- // email: args.email ?? undefined,
181
- // lastName: args.lastName ?? undefined,
182
- // firstName: args.firstName ?? undefined,
183
-
184
- // becomes this
185
-
186
- email: mappedArgs.email,
187
- lastName: mappedArgs.lastName,
188
- firstName: mappedArgs.firstName,
189
- })
190
- .returning()
191
- .then(assertFirstEntryExists);
192
- return user;
193
- });
194
-
195
- pubsub.updated(user.id);
196
-
197
- return db.query.user
198
- .findFirst(
199
- query(
200
- (await ctx.abilities.user.filter('read')).merge(
201
- {
202
- where: { id: user.id },
203
- }
204
- 1).query.single,
205
- ),
206
- )
207
- .then(assertFindFirstExists);
208
- },
209
- }),
210
- *
211
- *
212
- * ```
213
- */
214
- function mapNullFieldsToUndefined(obj) {
215
- return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
216
- }
217
- //#endregion
218
- //#region package.json
219
- var version = "0.25.3";
220
- //#endregion
221
75
  //#region lib/helpers/tableHelpers.ts
222
76
  const indexCache = /* @__PURE__ */ new WeakMap();
223
77
  function buildIndex(db) {
@@ -351,6 +205,8 @@ function sanitizeFilterValue(value) {
351
205
  const ACTION_FLAG_KEY = "__rumble_action";
352
206
  const columnFlagKey = (group) => `__rumble_columns_${group}`;
353
207
  const columnMaskKey = Symbol.for("rumble:columnMask");
208
+ const columnVisibilityKey = Symbol.for("rumble:columnVisibility");
209
+ const guardInputKey = Symbol.for("rumble:guardInput");
354
210
  const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
355
211
  const requestStateKey = Symbol.for("rumble:requestState");
356
212
  //#endregion
@@ -358,6 +214,7 @@ const requestStateKey = Symbol.for("rumble:requestState");
358
214
  function createRequestState() {
359
215
  return {
360
216
  masked: /* @__PURE__ */ new WeakMap(),
217
+ hidden: /* @__PURE__ */ new WeakMap(),
361
218
  actions: /* @__PURE__ */ new Map(),
362
219
  filterLoaders: /* @__PURE__ */ new Map()
363
220
  };
@@ -440,7 +297,7 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
440
297
  const toSQL = (table, wheres) => (0, drizzle_orm.or)(...wheres.map((where) => (0, drizzle_orm.relationsFilterToSQL)(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
441
298
  const uncorrelated = (table, wheres, group) => {
442
299
  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))})`;
300
+ return drizzle_orm.sql`exists ${db.select({ pk: inner[primaryKeyName] }).from(inner).where(drizzle_orm.sql`${inner[primaryKeyName]} = ${table[primaryKeyName]} and (${toSQL(inner, wheres)})`)}`;
444
301
  };
445
302
  return function resolveColumnAccess(action, filters) {
446
303
  const rules = filters.map((f) => ({
@@ -454,7 +311,14 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
454
311
  if (guaranteed.size === allColumnNames.length) return {
455
312
  columns,
456
313
  extras: void 0,
457
- mask: void 0
314
+ mask: void 0,
315
+ visibility: void 0
316
+ };
317
+ const visibility = (column) => {
318
+ if (guaranteed.has(column)) return true;
319
+ const wheres = rules.filter((rule) => rule.columns.has(column)).map((rule) => rule.where);
320
+ if (wheres.length === 0) return false;
321
+ return wheres.length === 1 ? wheres[0] : { OR: wheres };
458
322
  };
459
323
  const mask = {
460
324
  guaranteed,
@@ -474,7 +338,8 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
474
338
  return {
475
339
  columns,
476
340
  extras,
477
- mask
341
+ mask,
342
+ visibility
478
343
  };
479
344
  };
480
345
  }
@@ -533,12 +398,22 @@ function createColumnMasker({ actions }, tables) {
533
398
  const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
534
399
  maskRow(row, mask, hidden);
535
400
  state.masked.set(row, hidden.primaryKey);
401
+ state.hidden.set(row, hidden);
536
402
  }
537
403
  };
404
+ /**
405
+ * Whether masking hid any of the columns on the row. A hidden primary key
406
+ * counts for the columns it consists of.
407
+ */
408
+ const columnsHidden = (abilities, row, columns, primaryKey) => {
409
+ const hidden = stateOf(abilities).hidden.get(row);
410
+ return !!hidden && columns.some((column) => hidden.cleared.includes(column) || hidden.primaryKey && primaryKey.includes(column));
411
+ };
538
412
  return {
539
413
  maskColumns,
540
414
  maskedActions,
541
- primaryKeyHidden
415
+ primaryKeyHidden,
416
+ columnsHidden
542
417
  };
543
418
  }
544
419
  //#endregion
@@ -919,10 +794,11 @@ function createFilterResponder({ db, defaultLimit }, tableSchema) {
919
794
  /**
920
795
  * Packs the filters into a response object that can be applied for queries by the user
921
796
  */
922
- return function transformToResponse(queryFilters, columnMask) {
797
+ return function transformToResponse(queryFilters, columnMask, columnVisibility) {
923
798
  const ret = internalTransformer(queryFilters);
924
799
  ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
925
800
  ret[columnMaskKey] = columnMask;
801
+ ret[columnVisibilityKey] = columnVisibility;
926
802
  return ret;
927
803
  };
928
804
  }
@@ -1102,7 +978,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
1102
978
  const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
1103
979
  return mergeFilters(a, b, "OR");
1104
980
  });
1105
- const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
981
+ const { columns, extras, mask, visibility } = resolveColumnAccess(action, allQueryFilters);
1106
982
  if (columns) mergedFilters.columns = columns;
1107
983
  else delete mergedFilters.columns;
1108
984
  if (extras) mergedFilters.extras = {
@@ -1110,7 +986,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
1110
986
  ...extras
1111
987
  };
1112
988
  attributes[ATTR_ABILITIES_STATUS] = "applied";
1113
- return transformToResponse(mergedFilters, mask);
989
+ return transformToResponse(mergedFilters, mask, visibility);
1114
990
  };
1115
991
  return { withContext: (userContext) => {
1116
992
  const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
@@ -1130,6 +1006,140 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
1130
1006
  } };
1131
1007
  };
1132
1008
  //#endregion
1009
+ //#region lib/abilityBuilder/inputGuard.ts
1010
+ /** The ts names of the columns a relation joins on, on either side. */
1011
+ function relationColumnKeys(db, table, columns) {
1012
+ const known = tableHelper({
1013
+ db,
1014
+ table
1015
+ }).columns;
1016
+ return (columns ?? []).flatMap((column) => {
1017
+ const raw = column?._?.column ?? column;
1018
+ const key = Object.entries(known).find(([name, candidate]) => candidate === raw || name === raw?.name)?.[0];
1019
+ return key ? [key] : [];
1020
+ });
1021
+ }
1022
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1023
+ /**
1024
+ * Column masking hides values after the query ran, but a `where` or `orderBy`
1025
+ * argument is applied by the database on the real values: filtering for
1026
+ * `{ salary: { gt: 5000 } }` reveals which rows exceed it whether or not the
1027
+ * column may be read. This restricts such arguments to what the abilities
1028
+ * expose:
1029
+ *
1030
+ * - a column only matches on rows which expose it, for all other rows the
1031
+ * condition is false
1032
+ * - sorting by a column treats it as empty on rows which do not expose it
1033
+ * - a relation filter requires the columns the relation joins on to be
1034
+ * exposed, and is restricted to the rows the caller may read of the related
1035
+ * table, whose masked columns follow the same rules
1036
+ */
1037
+ function createInputGuard({ db, abilities, readActionOf }) {
1038
+ const visibilityOf = async (table, action) => (await abilities[table].filter(action))[columnVisibilityKey];
1039
+ const never = (table) => createBlockEverythingFilter(table, tableHelper({
1040
+ db,
1041
+ table
1042
+ })).where;
1043
+ /** The conditions under which the given columns are exposed. */
1044
+ const exposed = async (table, action, keys) => {
1045
+ const visibility = await visibilityOf(table, action);
1046
+ if (!visibility) return [];
1047
+ const conditions = [];
1048
+ for (const key of keys) {
1049
+ const condition = visibility(key);
1050
+ if (condition === true) continue;
1051
+ conditions.push(condition === false ? never(table) : condition);
1052
+ }
1053
+ return conditions;
1054
+ };
1055
+ /** The filter of a relation: the related rows the caller may read, exposing what it joins on. */
1056
+ const guardRelation = async (table, action, key, value, guarded, conditions) => {
1057
+ const relation = tableHelper({
1058
+ db,
1059
+ table
1060
+ }).relations[key];
1061
+ const target = relation.targetTableName;
1062
+ const targetAction = readActionOf(target);
1063
+ const targetKeys = relationColumnKeys(db, target, relation.targetColumns);
1064
+ conditions.push(...await exposed(table, action, relationColumnKeys(db, table, relation.sourceColumns)));
1065
+ const readable = (await abilities[target].filter(targetAction)).query.many.where;
1066
+ const related = [...readable ? [readable] : [], ...await exposed(target, targetAction, targetKeys)];
1067
+ const inner = isObject(value) ? await guardWhere(target, targetAction, value) : {};
1068
+ if (related.length === 0) guarded[key] = isObject(value) ? inner : value;
1069
+ else if (value === false) conditions.push({ NOT: { [key]: { AND: related } } });
1070
+ else guarded[key] = {
1071
+ ...inner,
1072
+ AND: [...inner.AND ?? [], ...related]
1073
+ };
1074
+ };
1075
+ const guardLogic = (table, action, value) => Array.isArray(value) ? Promise.all(value.map((v) => guardWhere(table, action, v))) : guardWhere(table, action, value);
1076
+ /** A single entry of a where: a logical operator, a relation or a column. */
1077
+ const guardEntry = async (table, action, [key, value], guarded, conditions) => {
1078
+ const schema = tableHelper({
1079
+ db,
1080
+ table
1081
+ });
1082
+ if (key === "AND" || key === "OR" || key === "NOT") guarded[key] = await guardLogic(table, action, value);
1083
+ else if (key in schema.relations) await guardRelation(table, action, key, value, guarded, conditions);
1084
+ else {
1085
+ guarded[key] = value;
1086
+ if (key in schema.columns) conditions.push(...await exposed(table, action, [key]));
1087
+ }
1088
+ };
1089
+ const guardWhere = async (table, action, where) => {
1090
+ if (!isObject(where)) return where;
1091
+ const guarded = {};
1092
+ const conditions = [];
1093
+ for (const entry of Object.entries(where)) await guardEntry(table, action, entry, guarded, conditions);
1094
+ if (conditions.length === 0) return guarded;
1095
+ return {
1096
+ ...guarded,
1097
+ AND: [...guarded.AND ?? [], ...conditions]
1098
+ };
1099
+ };
1100
+ const guardOrderBy = async (table, action, orderBy) => {
1101
+ if (!isObject(orderBy)) return orderBy;
1102
+ const visibility = await visibilityOf(table, action);
1103
+ if (!visibility) return orderBy;
1104
+ const entries = Object.entries(orderBy).filter(([, direction]) => direction);
1105
+ if (entries.every(([key]) => visibility(key) === true)) return orderBy;
1106
+ const schema = tableHelper({
1107
+ db,
1108
+ table
1109
+ });
1110
+ return (aliased, { asc, desc }) => entries.map(([key, direction]) => {
1111
+ const condition = visibility(key);
1112
+ const column = aliased[key];
1113
+ const value = condition === true ? column : condition === false ? null : drizzle_orm.sql`case when ${(0, drizzle_orm.relationsFilterToSQL)(aliased, sanitizeFilterValue(condition), schema.relations, db._.relations)} then ${column} end`;
1114
+ return (direction === "asc" ? asc : desc)(value ?? drizzle_orm.sql`null`);
1115
+ });
1116
+ };
1117
+ return {
1118
+ input: async (table, action, input) => ({
1119
+ ...input,
1120
+ where: await guardWhere(table, action, input.where),
1121
+ orderBy: await guardOrderBy(table, action, input.orderBy)
1122
+ }),
1123
+ exposed
1124
+ };
1125
+ }
1126
+ /**
1127
+ * Restricts client supplied `where` and `orderBy` arguments to what the
1128
+ * abilities of the request let the caller read, see `createInputGuard`.
1129
+ */
1130
+ function guardFilterInput(abilities, table, action, input) {
1131
+ const guard = abilities[guardInputKey];
1132
+ return guard ? guard.input(table, action, input) : Promise.resolve(input);
1133
+ }
1134
+ /**
1135
+ * The conditions a row of the related table has to meet to be reached through
1136
+ * a relation: the columns the relation joins on have to be exposed on it.
1137
+ */
1138
+ async function relationExposure(abilities, table, action, columns) {
1139
+ const guard = abilities[guardInputKey];
1140
+ return guard ? guard.exposed(table, action, columns) : [];
1141
+ }
1142
+ //#endregion
1133
1143
  //#region lib/abilityBuilder/tableBuilder.ts
1134
1144
  /**
1135
1145
  * Creates the registration API (allow/filter) of a single table.
@@ -1240,7 +1250,7 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1240
1250
  actions: settings.actions,
1241
1251
  isBuilt: () => hasBeenBuilt
1242
1252
  })]));
1243
- const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
1253
+ const { maskColumns, maskedActions, primaryKeyHidden, columnsHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
1244
1254
  const tableSchema = tableHelper({
1245
1255
  db,
1246
1256
  table: tableName
@@ -1263,6 +1273,8 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1263
1273
  maskedActions: (abilities, table) => maskedActions(abilities, table),
1264
1274
  /** Whether masking in this request hid the primary key of the row. */
1265
1275
  primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
1276
+ /** Whether masking in this request hid one of the columns on the row. */
1277
+ columnsHidden: (abilities, row, columns, primaryKey) => columnsHidden(abilities, row, columns, primaryKey),
1266
1278
  registerReadAction(table, action) {
1267
1279
  readActionPerTable.set(table, action);
1268
1280
  },
@@ -1279,6 +1291,11 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1279
1291
  const state = createRequestState();
1280
1292
  const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
1281
1293
  attachRequestState(abilities, state);
1294
+ Object.defineProperty(abilities, guardInputKey, { value: createInputGuard({
1295
+ db,
1296
+ abilities,
1297
+ readActionOf: (table) => readActionPerTable.get(table) ?? "read"
1298
+ }) });
1282
1299
  return abilities;
1283
1300
  };
1284
1301
  }
@@ -1286,6 +1303,152 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1286
1303
  };
1287
1304
  };
1288
1305
  //#endregion
1306
+ //#region lib/helpers/asserts.ts
1307
+ /**
1308
+ *
1309
+ * Helper function to map a drizzle findFirst query result,
1310
+ * which may be optional, to a correct drizzle type.
1311
+ *
1312
+ * @throws RumbleError
1313
+ *
1314
+ * @example
1315
+ *
1316
+ * ```ts
1317
+ * schemaBuilder.queryFields((t) => {
1318
+ return {
1319
+ findFirstUser: t.drizzleField({
1320
+ type: UserRef,
1321
+ resolve: (query, root, args, ctx, info) => {
1322
+ return (
1323
+ db.query.users
1324
+ .findFirst({
1325
+ ...query,
1326
+ where: (await ctx.abilities.users.filter("read")).single.where,
1327
+ })
1328
+ // note that we need to manually raise an error if the value is not found
1329
+ .then(assertFindFirstExists)
1330
+ );
1331
+ },
1332
+ }),
1333
+ };
1334
+ });
1335
+ * ```
1336
+ */
1337
+ const assertFindFirstExists = (value) => {
1338
+ if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
1339
+ return value;
1340
+ };
1341
+ /**
1342
+ *
1343
+ * Helper function to map a drizzle findFirst query result,
1344
+ * which may be optional, to a correct drizzle type.
1345
+ *
1346
+ * @throws RumbleError
1347
+ *
1348
+ * @example
1349
+ *
1350
+ * ```ts
1351
+ schemaBuilder.mutationFields((t) => {
1352
+ return {
1353
+ updateUsername: t.drizzleField({
1354
+ type: UserRef,
1355
+ args: {
1356
+ userId: t.arg.int({ required: true }),
1357
+ newName: t.arg.string({ required: true }),
1358
+ },
1359
+ resolve: (query, root, args, ctx, info) => {
1360
+ return db
1361
+ .update(schema.users)
1362
+ .set({
1363
+ name: args.newName,
1364
+ })
1365
+ .where(
1366
+ and(
1367
+ eq(schema.users.id, args.userId),
1368
+ (await ctx.abilities.users.filter("update")).single.where
1369
+ )
1370
+ )
1371
+ .returning({ id: schema.users.id, name: schema.users.name })
1372
+ // note that we need to manually raise an error if the value is not found
1373
+ .then(assertFirstEntryExists);
1374
+ },
1375
+ }),
1376
+ };
1377
+ });
1378
+ * ```
1379
+ */
1380
+ const assertFirstEntryExists = (value) => {
1381
+ const v = value.at(0);
1382
+ if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
1383
+ return v;
1384
+ };
1385
+ //#endregion
1386
+ //#region lib/helpers/mapNullFieldsToUndefined.ts
1387
+ /**
1388
+ * Helper to map null fields to undefined
1389
+ * @param obj The object to map
1390
+ * @returns The mapped object with all fields of 'null' transformed to 'undefined'
1391
+ *
1392
+ * This becomes useful for update mutations where you do not want to pass null (unset value in db)
1393
+ * but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
1394
+ * @example
1395
+ * ```ts
1396
+ * updateUser: t.drizzleField({
1397
+ type: User,
1398
+ args: {
1399
+ id: t.arg.string({ required: true }),
1400
+ email: t.arg.string(),
1401
+ lastName: t.arg.string(),
1402
+ firstName: t.arg.string(),
1403
+ },
1404
+ resolve: async (query, root, args, ctx, info) => {
1405
+ const mappedArgs = mapNullFieldsToUndefined(args);
1406
+ const user = await db.transaction(async (tx) => {
1407
+ const user = await tx
1408
+ .update(schema.user)
1409
+ .set({
1410
+
1411
+ // email: args.email ?? undefined,
1412
+ // lastName: args.lastName ?? undefined,
1413
+ // firstName: args.firstName ?? undefined,
1414
+
1415
+ // becomes this
1416
+
1417
+ email: mappedArgs.email,
1418
+ lastName: mappedArgs.lastName,
1419
+ firstName: mappedArgs.firstName,
1420
+ })
1421
+ .returning()
1422
+ .then(assertFirstEntryExists);
1423
+ return user;
1424
+ });
1425
+
1426
+ pubsub.updated(user.id);
1427
+
1428
+ return db.query.user
1429
+ .findFirst(
1430
+ query(
1431
+ (await ctx.abilities.user.filter('read')).merge(
1432
+ {
1433
+ where: { id: user.id },
1434
+ }
1435
+ 1).query.single,
1436
+ ),
1437
+ )
1438
+ .then(assertFindFirstExists);
1439
+ },
1440
+ }),
1441
+ *
1442
+ *
1443
+ * ```
1444
+ */
1445
+ function mapNullFieldsToUndefined(obj) {
1446
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
1447
+ }
1448
+ //#endregion
1449
+ //#region package.json
1450
+ var version = "0.26.1";
1451
+ //#endregion
1289
1452
  //#region lib/args/orderArg.ts
1290
1453
  const makeDefaultName$1 = (dbName) => `${(0, es_toolkit.capitalize)((0, drizzle_orm_casing.toCamelCase)(dbName.toString()))}OrderInputArgument`;
1291
1454
  const createOrderArgImplementer = ({ db, schemaBuilder }) => {
@@ -1712,6 +1875,8 @@ const createCountQueryImplementer = ({ db, schemaBuilder, whereArgImplementer, m
1712
1875
  resolve: async (_root, args, ctx, _info) => {
1713
1876
  if (isAllowed && !await isAllowed(ctx)) throw new RumbleErrorSafe("Not allowed to perform this action");
1714
1877
  deepSetProto(args);
1878
+ const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, { where: args.where });
1879
+ if (args.where) args.where = guarded.where;
1715
1880
  return db.select({ count: (0, drizzle_orm.count)() }).from(tableSchema.table).where((await ctx.abilities[table].filter(listAction)).merge(mapNullFieldsToUndefined(args)).sql.where).then(assertFirstEntryExists).then((r) => r.count);
1716
1881
  }
1717
1882
  }) };
@@ -2109,9 +2274,12 @@ function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
2109
2274
  args.extras = { search_distance: (table) => drizzle_orm.sql`${drizzle_orm.sql.join(columnsToSearch.map(([key]) => {
2110
2275
  return drizzle_orm.sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
2111
2276
  }), drizzle_orm.sql.raw(" + "))}` };
2112
- const originalOrderBy = (0, es_toolkit.cloneDeep)(args.orderBy);
2277
+ const originalOrderBy = typeof args.orderBy === "function" ? args.orderBy : (0, es_toolkit.cloneDeep)(args.orderBy);
2113
2278
  args.orderBy = (table) => {
2114
- const argsOrderBySQL = drizzle_orm.sql.join(Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
2279
+ const argsOrderBySQL = drizzle_orm.sql.join(typeof originalOrderBy === "function" ? originalOrderBy(table, {
2280
+ asc: drizzle_orm.asc,
2281
+ desc: drizzle_orm.desc
2282
+ }) : Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
2115
2283
  if (value === "asc") return drizzle_orm.sql`${table[key]} ASC`;
2116
2284
  else if (value === "desc") return drizzle_orm.sql`${table[key]} DESC`;
2117
2285
  else throw new Error(`Invalid value ${value} for orderBy`);
@@ -2195,6 +2363,13 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2195
2363
  fields: Object.keys(tableSchema.primaryKey),
2196
2364
  isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
2197
2365
  },
2366
+ maskRelations: {
2367
+ fields: Object.fromEntries(Object.entries(tableSchema.relations ?? {}).map(([key, relation]) => [key, {
2368
+ many: !(0, drizzle_orm.is)(relation, drizzle_orm.One),
2369
+ sourceColumns: relationColumnKeys(db, tableSchema.tsName, relation.sourceColumns)
2370
+ }])),
2371
+ isHidden: (context, row, columns) => abilityBuilder._.columnsHidden(context.abilities, row, columns, Object.keys(tableSchema.primaryKey))
2372
+ },
2198
2373
  fields: (t) => {
2199
2374
  const columns = tableSchema.columns;
2200
2375
  const configMap = /* @__PURE__ */ new Map();
@@ -2261,7 +2436,7 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2261
2436
  let filterSpecifier = "many";
2262
2437
  if ((0, drizzle_orm.is)(value, drizzle_orm.One)) {
2263
2438
  isMany = false;
2264
- nullable = value.optional || !value.sourceColumns.every((column) => column.notNull);
2439
+ nullable = value.optional || !value.sourceColumns.every((column) => (column._?.column ?? column).notNull);
2265
2440
  filterSpecifier = "single";
2266
2441
  }
2267
2442
  const subscribe = (subscriptions, _element) => {
@@ -2304,14 +2479,21 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2304
2479
  description: `Get the ${pluralize.default.plural(relationSchema.tsName)} related to this ${pluralize.default.singular(tableSchema.tsName)}`,
2305
2480
  query: async (args, ctx) => {
2306
2481
  args = JSON.parse(JSON.stringify(args));
2482
+ const guarded = await guardFilterInput(ctx.abilities, relationSchema.tsName, readAction, {
2483
+ where: args.where,
2484
+ orderBy: args.orderBy
2485
+ });
2486
+ if (args.where) args.where = guarded.where;
2487
+ if (args.orderBy) args.orderBy = guarded.orderBy;
2307
2488
  if (isMany) adjustQueryArgsForSearch({
2308
2489
  search,
2309
2490
  args,
2310
2491
  tableSchema: relationSchema,
2311
2492
  abilities: await ctx.abilities[relationSchema.tsName].filter(readAction)
2312
2493
  });
2494
+ const joined = await relationExposure(ctx.abilities, relationSchema.tsName, readAction, relationColumnKeys(db, relationSchema.tsName, value.targetColumns));
2313
2495
  const filter = (await ctx.abilities[relationSchema.tsName].filter(readAction)).merge({
2314
- where: args.where,
2496
+ where: joined.length ? { AND: [...args.where ? [args.where] : [], ...joined] } : args.where,
2315
2497
  limit: args.limit,
2316
2498
  extras: args.extras
2317
2499
  }).query[filterSpecifier];
@@ -2473,6 +2655,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
2473
2655
  args: manyArgs,
2474
2656
  resolve: async (query, _root, args, ctx, _info) => {
2475
2657
  deepSetProto(args);
2658
+ const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, {
2659
+ where: args.where,
2660
+ orderBy: args.orderBy
2661
+ });
2662
+ if (args.where) args.where = guarded.where;
2663
+ if (args.orderBy) args.orderBy = guarded.orderBy;
2476
2664
  adjustQueryArgsForSearch({
2477
2665
  search,
2478
2666
  args,
@@ -2800,12 +2988,19 @@ function registerPluginOnce(name, plugin) {
2800
2988
  const maskColumnsKey = "maskColumns";
2801
2989
  const maskColumnsActionKey = "maskColumnsAction";
2802
2990
  const maskPrimaryKeysKey = "maskPrimaryKeys";
2991
+ const maskRelationsKey = "maskRelations";
2803
2992
  const columnMaskPluginName = "ColumnMaskPlugin";
2804
2993
  //#endregion
2805
2994
  //#region lib/columnMaskPlugin/columnMaskPlugin.ts
2806
2995
  var ColumnMaskPlugin = class extends _pothos_core.BasePlugin {
2807
2996
  wrapResolve(resolver, fieldConfig) {
2808
- return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
2997
+ return this.maskResult(this.maskRelation(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig), fieldConfig);
2998
+ }
2999
+ maskRelation(resolver, fieldConfig) {
3000
+ const relations = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskRelationsKey];
3001
+ const relation = relations?.fields[fieldConfig.name];
3002
+ if (!relations || !relation) return resolver;
3003
+ return (parent, args, context, info) => relations.isHidden(context, parent, relation.sourceColumns) ? relation.many ? [] : null : resolver(parent, args, context, info);
2809
3004
  }
2810
3005
  maskPrimaryKey(resolver, fieldConfig) {
2811
3006
  const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
@@ -3625,6 +3820,7 @@ exports.RumbleError = RumbleError;
3625
3820
  exports.RumbleErrorSafe = RumbleErrorSafe;
3626
3821
  exports.assertFindFirstExists = assertFindFirstExists;
3627
3822
  exports.assertFirstEntryExists = assertFirstEntryExists;
3823
+ exports.guardFilterInput = guardFilterInput;
3628
3824
  exports.mapNullFieldsToUndefined = mapNullFieldsToUndefined;
3629
3825
  exports.normalizeEmailAddress = normalizeEmailAddress;
3630
3826
  exports.normalizePersonName = normalizePersonName;