@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.mjs CHANGED
@@ -1,15 +1,15 @@
1
1
  import { t as lazy } from "./lazy-C7IyM4M4.mjs";
2
- import { t as generateFromSchema } from "./generate-BAZg-eXa.mjs";
2
+ import { t as generateFromSchema } from "./generate-BySnFVve.mjs";
3
3
  import { createRequire } from "node:module";
4
+ import { One, aliasedTable, asc, count, desc, getTableColumns, getTableName, is, isTable, or, relationsFilterToSQL, sql } from "drizzle-orm";
4
5
  import { GraphQLError, GraphQLScalarType, GraphQLSchema, Kind, execute as defaultExecute, getOperationAST, print, subscribe as defaultSubscribe } from "graphql";
5
- import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
6
- import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
7
- import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
8
6
  import { capitalize, cloneDeep, debounce, merge, toMerged } from "es-toolkit";
9
- import { createPubSub, createYoga } from "graphql-yoga";
10
- import { One, aliasedTable, count, getTableColumns, getTableName, is, isTable, or, relationsFilterToSQL, sql } from "drizzle-orm";
7
+ import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
11
8
  import { ATTR_GRAPHQL_DOCUMENT, ATTR_GRAPHQL_OPERATION_NAME, ATTR_GRAPHQL_OPERATION_TYPE } from "@opentelemetry/semantic-conventions/incubating";
12
9
  import { AttributeNames, SpanNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
10
+ import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
11
+ import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
12
+ import { createPubSub, createYoga } from "graphql-yoga";
13
13
  import { toCamelCase } from "drizzle-orm/casing";
14
14
  import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, getTableConfig, isPgEnum } from "drizzle-orm/pg-core";
15
15
  import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable, getTableConfig as getTableConfig$1 } from "drizzle-orm/mysql-core";
@@ -42,152 +42,6 @@ var RumbleError = class extends Error {
42
42
  */
43
43
  var RumbleErrorSafe = class extends GraphQLError {};
44
44
  //#endregion
45
- //#region lib/helpers/asserts.ts
46
- /**
47
- *
48
- * Helper function to map a drizzle findFirst query result,
49
- * which may be optional, to a correct drizzle type.
50
- *
51
- * @throws RumbleError
52
- *
53
- * @example
54
- *
55
- * ```ts
56
- * schemaBuilder.queryFields((t) => {
57
- return {
58
- findFirstUser: t.drizzleField({
59
- type: UserRef,
60
- resolve: (query, root, args, ctx, info) => {
61
- return (
62
- db.query.users
63
- .findFirst({
64
- ...query,
65
- where: (await ctx.abilities.users.filter("read")).single.where,
66
- })
67
- // note that we need to manually raise an error if the value is not found
68
- .then(assertFindFirstExists)
69
- );
70
- },
71
- }),
72
- };
73
- });
74
- * ```
75
- */
76
- const assertFindFirstExists = (value) => {
77
- if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
78
- return value;
79
- };
80
- /**
81
- *
82
- * Helper function to map a drizzle findFirst query result,
83
- * which may be optional, to a correct drizzle type.
84
- *
85
- * @throws RumbleError
86
- *
87
- * @example
88
- *
89
- * ```ts
90
- schemaBuilder.mutationFields((t) => {
91
- return {
92
- updateUsername: t.drizzleField({
93
- type: UserRef,
94
- args: {
95
- userId: t.arg.int({ required: true }),
96
- newName: t.arg.string({ required: true }),
97
- },
98
- resolve: (query, root, args, ctx, info) => {
99
- return db
100
- .update(schema.users)
101
- .set({
102
- name: args.newName,
103
- })
104
- .where(
105
- and(
106
- eq(schema.users.id, args.userId),
107
- (await ctx.abilities.users.filter("update")).single.where
108
- )
109
- )
110
- .returning({ id: schema.users.id, name: schema.users.name })
111
- // note that we need to manually raise an error if the value is not found
112
- .then(assertFirstEntryExists);
113
- },
114
- }),
115
- };
116
- });
117
- * ```
118
- */
119
- const assertFirstEntryExists = (value) => {
120
- const v = value.at(0);
121
- if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
122
- return v;
123
- };
124
- //#endregion
125
- //#region lib/helpers/mapNullFieldsToUndefined.ts
126
- /**
127
- * Helper to map null fields to undefined
128
- * @param obj The object to map
129
- * @returns The mapped object with all fields of 'null' transformed to 'undefined'
130
- *
131
- * This becomes useful for update mutations where you do not want to pass null (unset value in db)
132
- * but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
133
- * @example
134
- * ```ts
135
- * updateUser: t.drizzleField({
136
- type: User,
137
- args: {
138
- id: t.arg.string({ required: true }),
139
- email: t.arg.string(),
140
- lastName: t.arg.string(),
141
- firstName: t.arg.string(),
142
- },
143
- resolve: async (query, root, args, ctx, info) => {
144
- const mappedArgs = mapNullFieldsToUndefined(args);
145
- const user = await db.transaction(async (tx) => {
146
- const user = await tx
147
- .update(schema.user)
148
- .set({
149
-
150
- // email: args.email ?? undefined,
151
- // lastName: args.lastName ?? undefined,
152
- // firstName: args.firstName ?? undefined,
153
-
154
- // becomes this
155
-
156
- email: mappedArgs.email,
157
- lastName: mappedArgs.lastName,
158
- firstName: mappedArgs.firstName,
159
- })
160
- .returning()
161
- .then(assertFirstEntryExists);
162
- return user;
163
- });
164
-
165
- pubsub.updated(user.id);
166
-
167
- return db.query.user
168
- .findFirst(
169
- query(
170
- (await ctx.abilities.user.filter('read')).merge(
171
- {
172
- where: { id: user.id },
173
- }
174
- 1).query.single,
175
- ),
176
- )
177
- .then(assertFindFirstExists);
178
- },
179
- }),
180
- *
181
- *
182
- * ```
183
- */
184
- function mapNullFieldsToUndefined(obj) {
185
- return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
186
- }
187
- //#endregion
188
- //#region package.json
189
- var version = "0.25.3";
190
- //#endregion
191
45
  //#region lib/helpers/tableHelpers.ts
192
46
  const indexCache = /* @__PURE__ */ new WeakMap();
193
47
  function buildIndex(db) {
@@ -321,6 +175,8 @@ function sanitizeFilterValue(value) {
321
175
  const ACTION_FLAG_KEY = "__rumble_action";
322
176
  const columnFlagKey = (group) => `__rumble_columns_${group}`;
323
177
  const columnMaskKey = Symbol.for("rumble:columnMask");
178
+ const columnVisibilityKey = Symbol.for("rumble:columnVisibility");
179
+ const guardInputKey = Symbol.for("rumble:guardInput");
324
180
  const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
325
181
  const requestStateKey = Symbol.for("rumble:requestState");
326
182
  //#endregion
@@ -328,6 +184,7 @@ const requestStateKey = Symbol.for("rumble:requestState");
328
184
  function createRequestState() {
329
185
  return {
330
186
  masked: /* @__PURE__ */ new WeakMap(),
187
+ hidden: /* @__PURE__ */ new WeakMap(),
331
188
  actions: /* @__PURE__ */ new Map(),
332
189
  filterLoaders: /* @__PURE__ */ new Map()
333
190
  };
@@ -410,7 +267,7 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
410
267
  const toSQL = (table, wheres) => or(...wheres.map((where) => relationsFilterToSQL(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
411
268
  const uncorrelated = (table, wheres, group) => {
412
269
  const inner = aliasedTable(tableSchema.table, `rumble_columns_${group}`);
413
- return sql`${table[primaryKeyName]} in (${db.select({ pk: inner[primaryKeyName] }).from(inner).where(toSQL(inner, wheres))})`;
270
+ return sql`exists ${db.select({ pk: inner[primaryKeyName] }).from(inner).where(sql`${inner[primaryKeyName]} = ${table[primaryKeyName]} and (${toSQL(inner, wheres)})`)}`;
414
271
  };
415
272
  return function resolveColumnAccess(action, filters) {
416
273
  const rules = filters.map((f) => ({
@@ -424,7 +281,14 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
424
281
  if (guaranteed.size === allColumnNames.length) return {
425
282
  columns,
426
283
  extras: void 0,
427
- mask: void 0
284
+ mask: void 0,
285
+ visibility: void 0
286
+ };
287
+ const visibility = (column) => {
288
+ if (guaranteed.has(column)) return true;
289
+ const wheres = rules.filter((rule) => rule.columns.has(column)).map((rule) => rule.where);
290
+ if (wheres.length === 0) return false;
291
+ return wheres.length === 1 ? wheres[0] : { OR: wheres };
428
292
  };
429
293
  const mask = {
430
294
  guaranteed,
@@ -444,7 +308,8 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
444
308
  return {
445
309
  columns,
446
310
  extras,
447
- mask
311
+ mask,
312
+ visibility
448
313
  };
449
314
  };
450
315
  }
@@ -503,12 +368,22 @@ function createColumnMasker({ actions }, tables) {
503
368
  const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
504
369
  maskRow(row, mask, hidden);
505
370
  state.masked.set(row, hidden.primaryKey);
371
+ state.hidden.set(row, hidden);
506
372
  }
507
373
  };
374
+ /**
375
+ * Whether masking hid any of the columns on the row. A hidden primary key
376
+ * counts for the columns it consists of.
377
+ */
378
+ const columnsHidden = (abilities, row, columns, primaryKey) => {
379
+ const hidden = stateOf(abilities).hidden.get(row);
380
+ return !!hidden && columns.some((column) => hidden.cleared.includes(column) || hidden.primaryKey && primaryKey.includes(column));
381
+ };
508
382
  return {
509
383
  maskColumns,
510
384
  maskedActions,
511
- primaryKeyHidden
385
+ primaryKeyHidden,
386
+ columnsHidden
512
387
  };
513
388
  }
514
389
  //#endregion
@@ -889,10 +764,11 @@ function createFilterResponder({ db, defaultLimit }, tableSchema) {
889
764
  /**
890
765
  * Packs the filters into a response object that can be applied for queries by the user
891
766
  */
892
- return function transformToResponse(queryFilters, columnMask) {
767
+ return function transformToResponse(queryFilters, columnMask, columnVisibility) {
893
768
  const ret = internalTransformer(queryFilters);
894
769
  ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
895
770
  ret[columnMaskKey] = columnMask;
771
+ ret[columnVisibilityKey] = columnVisibility;
896
772
  return ret;
897
773
  };
898
774
  }
@@ -1072,7 +948,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
1072
948
  const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
1073
949
  return mergeFilters(a, b, "OR");
1074
950
  });
1075
- const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
951
+ const { columns, extras, mask, visibility } = resolveColumnAccess(action, allQueryFilters);
1076
952
  if (columns) mergedFilters.columns = columns;
1077
953
  else delete mergedFilters.columns;
1078
954
  if (extras) mergedFilters.extras = {
@@ -1080,7 +956,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
1080
956
  ...extras
1081
957
  };
1082
958
  attributes[ATTR_ABILITIES_STATUS] = "applied";
1083
- return transformToResponse(mergedFilters, mask);
959
+ return transformToResponse(mergedFilters, mask, visibility);
1084
960
  };
1085
961
  return { withContext: (userContext) => {
1086
962
  const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
@@ -1100,6 +976,140 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
1100
976
  } };
1101
977
  };
1102
978
  //#endregion
979
+ //#region lib/abilityBuilder/inputGuard.ts
980
+ /** The ts names of the columns a relation joins on, on either side. */
981
+ function relationColumnKeys(db, table, columns) {
982
+ const known = tableHelper({
983
+ db,
984
+ table
985
+ }).columns;
986
+ return (columns ?? []).flatMap((column) => {
987
+ const raw = column?._?.column ?? column;
988
+ const key = Object.entries(known).find(([name, candidate]) => candidate === raw || name === raw?.name)?.[0];
989
+ return key ? [key] : [];
990
+ });
991
+ }
992
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
993
+ /**
994
+ * Column masking hides values after the query ran, but a `where` or `orderBy`
995
+ * argument is applied by the database on the real values: filtering for
996
+ * `{ salary: { gt: 5000 } }` reveals which rows exceed it whether or not the
997
+ * column may be read. This restricts such arguments to what the abilities
998
+ * expose:
999
+ *
1000
+ * - a column only matches on rows which expose it, for all other rows the
1001
+ * condition is false
1002
+ * - sorting by a column treats it as empty on rows which do not expose it
1003
+ * - a relation filter requires the columns the relation joins on to be
1004
+ * exposed, and is restricted to the rows the caller may read of the related
1005
+ * table, whose masked columns follow the same rules
1006
+ */
1007
+ function createInputGuard({ db, abilities, readActionOf }) {
1008
+ const visibilityOf = async (table, action) => (await abilities[table].filter(action))[columnVisibilityKey];
1009
+ const never = (table) => createBlockEverythingFilter(table, tableHelper({
1010
+ db,
1011
+ table
1012
+ })).where;
1013
+ /** The conditions under which the given columns are exposed. */
1014
+ const exposed = async (table, action, keys) => {
1015
+ const visibility = await visibilityOf(table, action);
1016
+ if (!visibility) return [];
1017
+ const conditions = [];
1018
+ for (const key of keys) {
1019
+ const condition = visibility(key);
1020
+ if (condition === true) continue;
1021
+ conditions.push(condition === false ? never(table) : condition);
1022
+ }
1023
+ return conditions;
1024
+ };
1025
+ /** The filter of a relation: the related rows the caller may read, exposing what it joins on. */
1026
+ const guardRelation = async (table, action, key, value, guarded, conditions) => {
1027
+ const relation = tableHelper({
1028
+ db,
1029
+ table
1030
+ }).relations[key];
1031
+ const target = relation.targetTableName;
1032
+ const targetAction = readActionOf(target);
1033
+ const targetKeys = relationColumnKeys(db, target, relation.targetColumns);
1034
+ conditions.push(...await exposed(table, action, relationColumnKeys(db, table, relation.sourceColumns)));
1035
+ const readable = (await abilities[target].filter(targetAction)).query.many.where;
1036
+ const related = [...readable ? [readable] : [], ...await exposed(target, targetAction, targetKeys)];
1037
+ const inner = isObject(value) ? await guardWhere(target, targetAction, value) : {};
1038
+ if (related.length === 0) guarded[key] = isObject(value) ? inner : value;
1039
+ else if (value === false) conditions.push({ NOT: { [key]: { AND: related } } });
1040
+ else guarded[key] = {
1041
+ ...inner,
1042
+ AND: [...inner.AND ?? [], ...related]
1043
+ };
1044
+ };
1045
+ const guardLogic = (table, action, value) => Array.isArray(value) ? Promise.all(value.map((v) => guardWhere(table, action, v))) : guardWhere(table, action, value);
1046
+ /** A single entry of a where: a logical operator, a relation or a column. */
1047
+ const guardEntry = async (table, action, [key, value], guarded, conditions) => {
1048
+ const schema = tableHelper({
1049
+ db,
1050
+ table
1051
+ });
1052
+ if (key === "AND" || key === "OR" || key === "NOT") guarded[key] = await guardLogic(table, action, value);
1053
+ else if (key in schema.relations) await guardRelation(table, action, key, value, guarded, conditions);
1054
+ else {
1055
+ guarded[key] = value;
1056
+ if (key in schema.columns) conditions.push(...await exposed(table, action, [key]));
1057
+ }
1058
+ };
1059
+ const guardWhere = async (table, action, where) => {
1060
+ if (!isObject(where)) return where;
1061
+ const guarded = {};
1062
+ const conditions = [];
1063
+ for (const entry of Object.entries(where)) await guardEntry(table, action, entry, guarded, conditions);
1064
+ if (conditions.length === 0) return guarded;
1065
+ return {
1066
+ ...guarded,
1067
+ AND: [...guarded.AND ?? [], ...conditions]
1068
+ };
1069
+ };
1070
+ const guardOrderBy = async (table, action, orderBy) => {
1071
+ if (!isObject(orderBy)) return orderBy;
1072
+ const visibility = await visibilityOf(table, action);
1073
+ if (!visibility) return orderBy;
1074
+ const entries = Object.entries(orderBy).filter(([, direction]) => direction);
1075
+ if (entries.every(([key]) => visibility(key) === true)) return orderBy;
1076
+ const schema = tableHelper({
1077
+ db,
1078
+ table
1079
+ });
1080
+ return (aliased, { asc, desc }) => entries.map(([key, direction]) => {
1081
+ const condition = visibility(key);
1082
+ const column = aliased[key];
1083
+ const value = condition === true ? column : condition === false ? null : sql`case when ${relationsFilterToSQL(aliased, sanitizeFilterValue(condition), schema.relations, db._.relations)} then ${column} end`;
1084
+ return (direction === "asc" ? asc : desc)(value ?? sql`null`);
1085
+ });
1086
+ };
1087
+ return {
1088
+ input: async (table, action, input) => ({
1089
+ ...input,
1090
+ where: await guardWhere(table, action, input.where),
1091
+ orderBy: await guardOrderBy(table, action, input.orderBy)
1092
+ }),
1093
+ exposed
1094
+ };
1095
+ }
1096
+ /**
1097
+ * Restricts client supplied `where` and `orderBy` arguments to what the
1098
+ * abilities of the request let the caller read, see `createInputGuard`.
1099
+ */
1100
+ function guardFilterInput(abilities, table, action, input) {
1101
+ const guard = abilities[guardInputKey];
1102
+ return guard ? guard.input(table, action, input) : Promise.resolve(input);
1103
+ }
1104
+ /**
1105
+ * The conditions a row of the related table has to meet to be reached through
1106
+ * a relation: the columns the relation joins on have to be exposed on it.
1107
+ */
1108
+ async function relationExposure(abilities, table, action, columns) {
1109
+ const guard = abilities[guardInputKey];
1110
+ return guard ? guard.exposed(table, action, columns) : [];
1111
+ }
1112
+ //#endregion
1103
1113
  //#region lib/abilityBuilder/tableBuilder.ts
1104
1114
  /**
1105
1115
  * Creates the registration API (allow/filter) of a single table.
@@ -1210,7 +1220,7 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1210
1220
  actions: settings.actions,
1211
1221
  isBuilt: () => hasBeenBuilt
1212
1222
  })]));
1213
- const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
1223
+ const { maskColumns, maskedActions, primaryKeyHidden, columnsHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
1214
1224
  const tableSchema = tableHelper({
1215
1225
  db,
1216
1226
  table: tableName
@@ -1233,6 +1243,8 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1233
1243
  maskedActions: (abilities, table) => maskedActions(abilities, table),
1234
1244
  /** Whether masking in this request hid the primary key of the row. */
1235
1245
  primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
1246
+ /** Whether masking in this request hid one of the columns on the row. */
1247
+ columnsHidden: (abilities, row, columns, primaryKey) => columnsHidden(abilities, row, columns, primaryKey),
1236
1248
  registerReadAction(table, action) {
1237
1249
  readActionPerTable.set(table, action);
1238
1250
  },
@@ -1249,6 +1261,11 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1249
1261
  const state = createRequestState();
1250
1262
  const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
1251
1263
  attachRequestState(abilities, state);
1264
+ Object.defineProperty(abilities, guardInputKey, { value: createInputGuard({
1265
+ db,
1266
+ abilities,
1267
+ readActionOf: (table) => readActionPerTable.get(table) ?? "read"
1268
+ }) });
1252
1269
  return abilities;
1253
1270
  };
1254
1271
  }
@@ -1256,6 +1273,152 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
1256
1273
  };
1257
1274
  };
1258
1275
  //#endregion
1276
+ //#region lib/helpers/asserts.ts
1277
+ /**
1278
+ *
1279
+ * Helper function to map a drizzle findFirst query result,
1280
+ * which may be optional, to a correct drizzle type.
1281
+ *
1282
+ * @throws RumbleError
1283
+ *
1284
+ * @example
1285
+ *
1286
+ * ```ts
1287
+ * schemaBuilder.queryFields((t) => {
1288
+ return {
1289
+ findFirstUser: t.drizzleField({
1290
+ type: UserRef,
1291
+ resolve: (query, root, args, ctx, info) => {
1292
+ return (
1293
+ db.query.users
1294
+ .findFirst({
1295
+ ...query,
1296
+ where: (await ctx.abilities.users.filter("read")).single.where,
1297
+ })
1298
+ // note that we need to manually raise an error if the value is not found
1299
+ .then(assertFindFirstExists)
1300
+ );
1301
+ },
1302
+ }),
1303
+ };
1304
+ });
1305
+ * ```
1306
+ */
1307
+ const assertFindFirstExists = (value) => {
1308
+ if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
1309
+ return value;
1310
+ };
1311
+ /**
1312
+ *
1313
+ * Helper function to map a drizzle findFirst query result,
1314
+ * which may be optional, to a correct drizzle type.
1315
+ *
1316
+ * @throws RumbleError
1317
+ *
1318
+ * @example
1319
+ *
1320
+ * ```ts
1321
+ schemaBuilder.mutationFields((t) => {
1322
+ return {
1323
+ updateUsername: t.drizzleField({
1324
+ type: UserRef,
1325
+ args: {
1326
+ userId: t.arg.int({ required: true }),
1327
+ newName: t.arg.string({ required: true }),
1328
+ },
1329
+ resolve: (query, root, args, ctx, info) => {
1330
+ return db
1331
+ .update(schema.users)
1332
+ .set({
1333
+ name: args.newName,
1334
+ })
1335
+ .where(
1336
+ and(
1337
+ eq(schema.users.id, args.userId),
1338
+ (await ctx.abilities.users.filter("update")).single.where
1339
+ )
1340
+ )
1341
+ .returning({ id: schema.users.id, name: schema.users.name })
1342
+ // note that we need to manually raise an error if the value is not found
1343
+ .then(assertFirstEntryExists);
1344
+ },
1345
+ }),
1346
+ };
1347
+ });
1348
+ * ```
1349
+ */
1350
+ const assertFirstEntryExists = (value) => {
1351
+ const v = value.at(0);
1352
+ if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
1353
+ return v;
1354
+ };
1355
+ //#endregion
1356
+ //#region lib/helpers/mapNullFieldsToUndefined.ts
1357
+ /**
1358
+ * Helper to map null fields to undefined
1359
+ * @param obj The object to map
1360
+ * @returns The mapped object with all fields of 'null' transformed to 'undefined'
1361
+ *
1362
+ * This becomes useful for update mutations where you do not want to pass null (unset value in db)
1363
+ * but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
1364
+ * @example
1365
+ * ```ts
1366
+ * updateUser: t.drizzleField({
1367
+ type: User,
1368
+ args: {
1369
+ id: t.arg.string({ required: true }),
1370
+ email: t.arg.string(),
1371
+ lastName: t.arg.string(),
1372
+ firstName: t.arg.string(),
1373
+ },
1374
+ resolve: async (query, root, args, ctx, info) => {
1375
+ const mappedArgs = mapNullFieldsToUndefined(args);
1376
+ const user = await db.transaction(async (tx) => {
1377
+ const user = await tx
1378
+ .update(schema.user)
1379
+ .set({
1380
+
1381
+ // email: args.email ?? undefined,
1382
+ // lastName: args.lastName ?? undefined,
1383
+ // firstName: args.firstName ?? undefined,
1384
+
1385
+ // becomes this
1386
+
1387
+ email: mappedArgs.email,
1388
+ lastName: mappedArgs.lastName,
1389
+ firstName: mappedArgs.firstName,
1390
+ })
1391
+ .returning()
1392
+ .then(assertFirstEntryExists);
1393
+ return user;
1394
+ });
1395
+
1396
+ pubsub.updated(user.id);
1397
+
1398
+ return db.query.user
1399
+ .findFirst(
1400
+ query(
1401
+ (await ctx.abilities.user.filter('read')).merge(
1402
+ {
1403
+ where: { id: user.id },
1404
+ }
1405
+ 1).query.single,
1406
+ ),
1407
+ )
1408
+ .then(assertFindFirstExists);
1409
+ },
1410
+ }),
1411
+ *
1412
+ *
1413
+ * ```
1414
+ */
1415
+ function mapNullFieldsToUndefined(obj) {
1416
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
1417
+ }
1418
+ //#endregion
1419
+ //#region package.json
1420
+ var version = "0.26.1";
1421
+ //#endregion
1259
1422
  //#region lib/args/orderArg.ts
1260
1423
  const makeDefaultName$1 = (dbName) => `${capitalize(toCamelCase(dbName.toString()))}OrderInputArgument`;
1261
1424
  const createOrderArgImplementer = ({ db, schemaBuilder }) => {
@@ -1682,6 +1845,8 @@ const createCountQueryImplementer = ({ db, schemaBuilder, whereArgImplementer, m
1682
1845
  resolve: async (_root, args, ctx, _info) => {
1683
1846
  if (isAllowed && !await isAllowed(ctx)) throw new RumbleErrorSafe("Not allowed to perform this action");
1684
1847
  deepSetProto(args);
1848
+ const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, { where: args.where });
1849
+ if (args.where) args.where = guarded.where;
1685
1850
  return db.select({ count: count() }).from(tableSchema.table).where((await ctx.abilities[table].filter(listAction)).merge(mapNullFieldsToUndefined(args)).sql.where).then(assertFirstEntryExists).then((r) => r.count);
1686
1851
  }
1687
1852
  }) };
@@ -2079,9 +2244,12 @@ function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
2079
2244
  args.extras = { search_distance: (table) => sql`${sql.join(columnsToSearch.map(([key]) => {
2080
2245
  return sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
2081
2246
  }), sql.raw(" + "))}` };
2082
- const originalOrderBy = cloneDeep(args.orderBy);
2247
+ const originalOrderBy = typeof args.orderBy === "function" ? args.orderBy : cloneDeep(args.orderBy);
2083
2248
  args.orderBy = (table) => {
2084
- const argsOrderBySQL = sql.join(Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
2249
+ const argsOrderBySQL = sql.join(typeof originalOrderBy === "function" ? originalOrderBy(table, {
2250
+ asc,
2251
+ desc
2252
+ }) : Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
2085
2253
  if (value === "asc") return sql`${table[key]} ASC`;
2086
2254
  else if (value === "desc") return sql`${table[key]} DESC`;
2087
2255
  else throw new Error(`Invalid value ${value} for orderBy`);
@@ -2165,6 +2333,13 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2165
2333
  fields: Object.keys(tableSchema.primaryKey),
2166
2334
  isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
2167
2335
  },
2336
+ maskRelations: {
2337
+ fields: Object.fromEntries(Object.entries(tableSchema.relations ?? {}).map(([key, relation]) => [key, {
2338
+ many: !is(relation, One),
2339
+ sourceColumns: relationColumnKeys(db, tableSchema.tsName, relation.sourceColumns)
2340
+ }])),
2341
+ isHidden: (context, row, columns) => abilityBuilder._.columnsHidden(context.abilities, row, columns, Object.keys(tableSchema.primaryKey))
2342
+ },
2168
2343
  fields: (t) => {
2169
2344
  const columns = tableSchema.columns;
2170
2345
  const configMap = /* @__PURE__ */ new Map();
@@ -2231,7 +2406,7 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2231
2406
  let filterSpecifier = "many";
2232
2407
  if (is(value, One)) {
2233
2408
  isMany = false;
2234
- nullable = value.optional || !value.sourceColumns.every((column) => column.notNull);
2409
+ nullable = value.optional || !value.sourceColumns.every((column) => (column._?.column ?? column).notNull);
2235
2410
  filterSpecifier = "single";
2236
2411
  }
2237
2412
  const subscribe = (subscriptions, _element) => {
@@ -2274,14 +2449,21 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2274
2449
  description: `Get the ${pluralize.plural(relationSchema.tsName)} related to this ${pluralize.singular(tableSchema.tsName)}`,
2275
2450
  query: async (args, ctx) => {
2276
2451
  args = JSON.parse(JSON.stringify(args));
2452
+ const guarded = await guardFilterInput(ctx.abilities, relationSchema.tsName, readAction, {
2453
+ where: args.where,
2454
+ orderBy: args.orderBy
2455
+ });
2456
+ if (args.where) args.where = guarded.where;
2457
+ if (args.orderBy) args.orderBy = guarded.orderBy;
2277
2458
  if (isMany) adjustQueryArgsForSearch({
2278
2459
  search,
2279
2460
  args,
2280
2461
  tableSchema: relationSchema,
2281
2462
  abilities: await ctx.abilities[relationSchema.tsName].filter(readAction)
2282
2463
  });
2464
+ const joined = await relationExposure(ctx.abilities, relationSchema.tsName, readAction, relationColumnKeys(db, relationSchema.tsName, value.targetColumns));
2283
2465
  const filter = (await ctx.abilities[relationSchema.tsName].filter(readAction)).merge({
2284
- where: args.where,
2466
+ where: joined.length ? { AND: [...args.where ? [args.where] : [], ...joined] } : args.where,
2285
2467
  limit: args.limit,
2286
2468
  extras: args.extras
2287
2469
  }).query[filterSpecifier];
@@ -2443,6 +2625,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
2443
2625
  args: manyArgs,
2444
2626
  resolve: async (query, _root, args, ctx, _info) => {
2445
2627
  deepSetProto(args);
2628
+ const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, {
2629
+ where: args.where,
2630
+ orderBy: args.orderBy
2631
+ });
2632
+ if (args.where) args.where = guarded.where;
2633
+ if (args.orderBy) args.orderBy = guarded.orderBy;
2446
2634
  adjustQueryArgsForSearch({
2447
2635
  search,
2448
2636
  args,
@@ -2770,12 +2958,19 @@ function registerPluginOnce(name, plugin) {
2770
2958
  const maskColumnsKey = "maskColumns";
2771
2959
  const maskColumnsActionKey = "maskColumnsAction";
2772
2960
  const maskPrimaryKeysKey = "maskPrimaryKeys";
2961
+ const maskRelationsKey = "maskRelations";
2773
2962
  const columnMaskPluginName = "ColumnMaskPlugin";
2774
2963
  //#endregion
2775
2964
  //#region lib/columnMaskPlugin/columnMaskPlugin.ts
2776
2965
  var ColumnMaskPlugin = class extends BasePlugin {
2777
2966
  wrapResolve(resolver, fieldConfig) {
2778
- return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
2967
+ return this.maskResult(this.maskRelation(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig), fieldConfig);
2968
+ }
2969
+ maskRelation(resolver, fieldConfig) {
2970
+ const relations = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskRelationsKey];
2971
+ const relation = relations?.fields[fieldConfig.name];
2972
+ if (!relations || !relation) return resolver;
2973
+ return (parent, args, context, info) => relations.isHidden(context, parent, relation.sourceColumns) ? relation.many ? [] : null : resolver(parent, args, context, info);
2779
2974
  }
2780
2975
  maskPrimaryKey(resolver, fieldConfig) {
2781
2976
  const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
@@ -3591,6 +3786,6 @@ export const r = rumble({
3591
3786
  };
3592
3787
  };
3593
3788
  //#endregion
3594
- export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, mapNullFieldsToUndefined, normalizeEmailAddress, normalizePersonName, parsePhoneNumber, rumble };
3789
+ export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, guardFilterInput, mapNullFieldsToUndefined, normalizeEmailAddress, normalizePersonName, parsePhoneNumber, rumble };
3595
3790
 
3596
3791
  //# sourceMappingURL=index.mjs.map