@m1212e/rumble 0.23.24 → 0.24.0

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
@@ -62,7 +62,7 @@ return (
62
62
  db.query.users
63
63
  .findFirst({
64
64
  ...query,
65
- where: ctx.abilities.users.filter("read").single.where,
65
+ where: (await ctx.abilities.users.filter("read")).single.where,
66
66
  })
67
67
  // note that we need to manually raise an error if the value is not found
68
68
  .then(assertFindFirstExists)
@@ -104,7 +104,7 @@ name: args.newName,
104
104
  .where(
105
105
  and(
106
106
  eq(schema.users.id, args.userId),
107
- ctx.abilities.users.filter("update").single.where
107
+ (await ctx.abilities.users.filter("update")).single.where
108
108
  )
109
109
  )
110
110
  .returning({ id: schema.users.id, name: schema.users.name })
@@ -167,7 +167,7 @@ pubsub.updated(user.id);
167
167
  return db.query.user
168
168
  .findFirst(
169
169
  query(
170
- ctx.abilities.user.filter('read').merge(
170
+ (await ctx.abilities.user.filter('read')).merge(
171
171
  {
172
172
  where: { id: user.id },
173
173
  }
@@ -186,7 +186,7 @@ function mapNullFieldsToUndefined(obj) {
186
186
  }
187
187
  //#endregion
188
188
  //#region package.json
189
- var version = "0.23.24";
189
+ var version = "0.24.0";
190
190
  //#endregion
191
191
  //#region lib/helpers/errorLogging.ts
192
192
  function hasMessage(error) {
@@ -618,7 +618,6 @@ function attributeValue(value) {
618
618
  case "boolean": return value;
619
619
  case "bigint": return value.toString();
620
620
  case "undefined": return "null";
621
- default: break;
622
621
  }
623
622
  if (value === null) return "null";
624
623
  if (Array.isArray(value) && isHomogeneousPrimitiveArray(value)) return value;
@@ -656,11 +655,28 @@ function durationMs(start) {
656
655
  //#endregion
657
656
  //#region lib/abilityBuilder.ts
658
657
  function isDynamicQueryFilter(filter) {
659
- return typeof filter === "function" && filter.constructor.name !== "AsyncFunction";
658
+ return typeof filter === "function";
660
659
  }
661
660
  function isStaticQueryFilter(filter) {
662
661
  return typeof filter !== "function";
663
662
  }
663
+ /**
664
+ * filter() is async. Without type checking (plain JS, `any`) a forgotten await would
665
+ * hand the promise itself to drizzle
666
+ */
667
+ function guardAgainstMissingAwait(promise) {
668
+ for (const key of [
669
+ "query",
670
+ "sql",
671
+ "merge"
672
+ ]) Object.defineProperty(promise, key, {
673
+ enumerable: true,
674
+ get() {
675
+ throw new RumbleError(`Tried to access "${key}" on the result of abilities.<table>.filter(...) which is a Promise. filter() is async, did you forget to await it?`);
676
+ }
677
+ });
678
+ return promise;
679
+ }
664
680
  const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
665
681
  const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
666
682
  if (logger) logger.warn({
@@ -716,8 +732,10 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
716
732
  */
717
733
  when: (queryFilter) => {
718
734
  for (const action of actions) {
719
- if (queryFilters.get(action) === "unrestricted") if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
720
- else continue;
735
+ if (queryFilters.get(action) === "unrestricted") {
736
+ if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
737
+ else continue;
738
+ }
721
739
  queryFilters.get(action).push(queryFilter);
722
740
  }
723
741
  } };
@@ -822,7 +840,7 @@ by: (explicitFilter) => {
822
840
  * @example
823
841
  * ```ts
824
842
  * author: t.relation("author", {
825
- * query: (_args, ctx) => ctx.abilities.users.filter("read").query.single,
843
+ * query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
826
844
  * }),
827
845
  * ´´´
828
846
  */
@@ -860,7 +878,7 @@ by: (explicitFilter) => {
860
878
  * .where(
861
879
  * and(
862
880
  * eq(schema.users.id, args.userId),
863
- * ctx.abilities.users.filter("update").sql.where,
881
+ * (await ctx.abilities.users.filter("update")).sql.where,
864
882
  * ),
865
883
  * );
866
884
  * ```
@@ -876,7 +894,7 @@ by: (explicitFilter) => {
876
894
  * @example
877
895
  * ```ts
878
896
  * author: t.relation("author", {
879
- * query: (_args, ctx) => ctx.abilities.users.filter("read").query.single,
897
+ * query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
880
898
  * }),
881
899
  * ´´´
882
900
  */
@@ -912,7 +930,7 @@ by: (explicitFilter) => {
912
930
  * .where(
913
931
  * and(
914
932
  * eq(schema.users.id, args.userId),
915
- * ctx.abilities.users.filter("update").sql.where,
933
+ * (await ctx.abilities.users.filter("update")).sql.where,
916
934
  * ),
917
935
  * );
918
936
  * ```
@@ -935,8 +953,8 @@ by: (explicitFilter) => {
935
953
  return ret;
936
954
  }
937
955
  return { withContext: (userContext) => {
938
- return { filter: (action) => {
939
- const assembleAbilities = (attributes) => {
956
+ const prepare = (action) => {
957
+ const assembleAbilities = async (attributes) => {
940
958
  const filters = queryFilters.get(action);
941
959
  if (filters === "unrestricted") {
942
960
  attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
@@ -947,19 +965,12 @@ by: (explicitFilter) => {
947
965
  nothingRegisteredWarningLogger(String(tableName), action);
948
966
  return transformToResponse(blockEverythingFilter);
949
967
  }
950
- const dynamicResults = new Array(dynamicQueryFilters[action].length);
951
- let filtersReturned = 0;
952
- for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
953
- const func = dynamicQueryFilters[action][i];
954
- const result = func(userContext);
955
- if (result === "allow") {
956
- attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
957
- return transformToResponse();
958
- }
959
- if (result === void 0) continue;
960
- dynamicResults[filtersReturned++] = result;
968
+ const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
969
+ if (rawResults.includes("allow")) {
970
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
971
+ return transformToResponse();
961
972
  }
962
- dynamicResults.length = filtersReturned;
973
+ const dynamicResults = rawResults.filter((r) => r !== void 0);
963
974
  attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
964
975
  attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
965
976
  const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
@@ -974,13 +985,13 @@ by: (explicitFilter) => {
974
985
  attributes[ATTR_ABILITIES_STATUS] = "applied";
975
986
  return transformToResponse(mergedFilters);
976
987
  };
977
- const run = (span) => {
988
+ const run = async (span) => {
978
989
  const attributes = {
979
990
  [ATTR_TABLE]: String(tableName),
980
991
  [ATTR_ACTION]: action
981
992
  };
982
993
  try {
983
- const result = assembleAbilities(attributes);
994
+ const result = await assembleAbilities(attributes);
984
995
  log?.debug({
985
996
  ...attributes,
986
997
  ...traceCorrelationFields(telemetryConfig, span)
@@ -998,14 +1009,23 @@ by: (explicitFilter) => {
998
1009
  span?.setAttributes(attributes);
999
1010
  }
1000
1011
  };
1001
- if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, (span) => {
1012
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
1002
1013
  try {
1003
- return run(span);
1014
+ return await run(span);
1004
1015
  } finally {
1005
1016
  span.end();
1006
1017
  }
1007
1018
  });
1008
1019
  return run();
1020
+ };
1021
+ const cache = /* @__PURE__ */ new Map();
1022
+ return { filter: (action) => {
1023
+ let prepared = cache.get(action);
1024
+ if (!prepared) {
1025
+ prepared = guardAgainstMissingAwait(prepare(action));
1026
+ cache.set(action, prepared);
1027
+ }
1028
+ return prepared;
1009
1029
  } };
1010
1030
  } };
1011
1031
  };
@@ -1088,16 +1108,18 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
1088
1108
  if (enumObject) {
1089
1109
  enumSchemaName = enumObject.enumName;
1090
1110
  enumValues = enumObject.enumValues;
1091
- } else if (enumColumn) if (is(enumColumn, MySqlEnumColumn) || is(enumColumn, MySqlEnumObjectColumn)) {
1092
- enumValues = enumColumn.enumValues;
1093
- const colTable = enumColumn.table;
1094
- const tableName = colTable && isTable(colTable) ? getTableName(colTable) : void 0;
1095
- enumSchemaName = tableName ? `${tableName}_${enumColumn.name}` : enumColumn.name;
1096
- } else {
1097
- const pgEnum = enumColumn.enum;
1098
- if (!pgEnum) throw new RumbleError(`Could not find enum definition on column. Make sure the column is a pgEnum column.`);
1099
- enumSchemaName = pgEnum.enumName;
1100
- enumValues = enumColumn.enumValues;
1111
+ } else if (enumColumn) {
1112
+ if (is(enumColumn, MySqlEnumColumn) || is(enumColumn, MySqlEnumObjectColumn)) {
1113
+ enumValues = enumColumn.enumValues;
1114
+ const colTable = enumColumn.table;
1115
+ const tableName = colTable && isTable(colTable) ? getTableName(colTable) : void 0;
1116
+ enumSchemaName = tableName ? `${tableName}_${enumColumn.name}` : enumColumn.name;
1117
+ } else {
1118
+ const pgEnum = enumColumn.enum;
1119
+ if (!pgEnum) throw new RumbleError(`Could not find enum definition on column. Make sure the column is a pgEnum column.`);
1120
+ enumSchemaName = pgEnum.enumName;
1121
+ enumValues = enumColumn.enumValues;
1122
+ }
1101
1123
  }
1102
1124
  if (!enumSchemaName || !enumValues) throw new RumbleError("Could not determine enum structure!");
1103
1125
  const graphqlImplementationName = refName ?? `${capitalize(toCamelCase(enumSchemaName))}Enum`;
@@ -1118,8 +1140,10 @@ function mapSQLTypeToGraphQLType({ sqlType, fieldName }) {
1118
1140
  if (isIntLikeSQLTypeString(normalized)) ret = "Int";
1119
1141
  if (isBigIntLikeSQLTypeString(normalized)) ret = "BigInt";
1120
1142
  if (isFloatLikeSQLTypeString(normalized)) ret = "Float";
1121
- if (isStringLikeSQLTypeString(normalized)) if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
1122
- else ret = "String";
1143
+ if (isStringLikeSQLTypeString(normalized)) {
1144
+ if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
1145
+ else ret = "String";
1146
+ }
1123
1147
  if (isIDLikeSQLTypeString(normalized)) ret = "ID";
1124
1148
  if (isBooleanSQLTypeString(normalized)) ret = "Boolean";
1125
1149
  if (isDateTimeLikeSQLTypeString(normalized)) ret = "DateTime";
@@ -1367,8 +1391,9 @@ const clientCreatorImplementer = ({ builtSchema }) => {
1367
1391
  useExternalUrqlClient,
1368
1392
  removeExisting
1369
1393
  })}`);
1394
+ const schema = builtSchema();
1370
1395
  await generateFromSchema({
1371
- schema: builtSchema(),
1396
+ schema,
1372
1397
  outputPath,
1373
1398
  rumbleImportPath,
1374
1399
  apiUrl,
@@ -1440,7 +1465,7 @@ const createCountQueryImplementer = ({ db, schemaBuilder, whereArgImplementer, m
1440
1465
  resolve: async (_root, args, ctx, _info) => {
1441
1466
  if (isAllowed && !await isAllowed(ctx)) throw new RumbleErrorSafe("Not allowed to perform this action");
1442
1467
  deepSetProto(args);
1443
- return db.select({ count: count() }).from(tableSchema.table).where(ctx.abilities[table].filter(listAction).merge(mapNullFieldsToUndefined(args)).sql.where).then(assertFirstEntryExists).then((r) => r.count);
1468
+ 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);
1444
1469
  }
1445
1470
  }) };
1446
1471
  });
@@ -2018,15 +2043,15 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
2018
2043
  subscribe,
2019
2044
  nullable,
2020
2045
  description: `Get the ${pluralize.plural(relationSchema.tsName)} related to this ${pluralize.singular(tableSchema.tsName)}`,
2021
- query: (args, ctx) => {
2046
+ query: async (args, ctx) => {
2022
2047
  args = JSON.parse(JSON.stringify(args));
2023
2048
  if (isMany) adjustQueryArgsForSearch({
2024
2049
  search,
2025
2050
  args,
2026
2051
  tableSchema: relationSchema,
2027
- abilities: ctx.abilities[relationSchema.tsName].filter(readAction)
2052
+ abilities: await ctx.abilities[relationSchema.tsName].filter(readAction)
2028
2053
  });
2029
- const filter = ctx.abilities[relationSchema.tsName].filter(readAction).merge({
2054
+ const filter = (await ctx.abilities[relationSchema.tsName].filter(readAction)).merge({
2030
2055
  where: args.where,
2031
2056
  limit: args.limit,
2032
2057
  extras: args.extras
@@ -2186,16 +2211,16 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
2186
2211
  });
2187
2212
  },
2188
2213
  args: manyArgs,
2189
- resolve: (query, _root, args, ctx, _info) => {
2214
+ resolve: async (query, _root, args, ctx, _info) => {
2190
2215
  deepSetProto(args);
2191
2216
  adjustQueryArgsForSearch({
2192
2217
  search,
2193
2218
  args,
2194
2219
  tableSchema,
2195
- abilities: ctx.abilities[table].filter(listAction)
2220
+ abilities: await ctx.abilities[table].filter(listAction)
2196
2221
  });
2197
2222
  const mappedArgs = mapNullFieldsToUndefined(args);
2198
- const filter = ctx.abilities[table].filter(listAction).merge(mappedArgs).query.many;
2223
+ const filter = (await ctx.abilities[table].filter(listAction)).merge(mappedArgs).query.many;
2199
2224
  if (mappedArgs.offset) filter.offset = mappedArgs.offset;
2200
2225
  if (mappedArgs.orderBy) filter.orderBy = mappedArgs.orderBy;
2201
2226
  const queryArgs = query(filter);
@@ -2223,9 +2248,9 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
2223
2248
  smartSubscription: true,
2224
2249
  description: `Get a single ${singularName} by ID`,
2225
2250
  args: { id: t.arg.id({ required: true }) },
2226
- resolve: (query, _root, args, ctx, _info) => {
2251
+ resolve: async (query, _root, args, ctx, _info) => {
2227
2252
  deepSetProto(args);
2228
- const filter = ctx.abilities[table].filter(readAction).merge({ where: { [primaryKeyField.name]: args.id } }).query.single;
2253
+ const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [primaryKeyField.name]: args.id } }).query.single;
2229
2254
  const queryArgs = query(filter);
2230
2255
  if (filter.columns) queryArgs.columns = filter.columns;
2231
2256
  return findFirst ? findFirst({