@m1212e/rumble 0.22.2 → 0.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as lazy } from "./lazy-C7IyM4M4.mjs";
2
2
  import { t as generateFromSchema } from "./generate-0QFVc3Sg.mjs";
3
- import { GraphQLError } from "graphql";
3
+ import { GraphQLError, GraphQLSchema } from "graphql";
4
4
  import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
5
5
  import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
6
6
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -10,14 +10,14 @@ import { createPubSub, createYoga } from "graphql-yoga";
10
10
  import { One, count, getTableColumns, getTableName, is, isTable, relationsFilterToSQL, sql } from "drizzle-orm";
11
11
  import { toCamelCase } from "drizzle-orm/casing";
12
12
  import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, isPgEnum } from "drizzle-orm/pg-core";
13
+ import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable } from "drizzle-orm/mysql-core";
13
14
  import pluralize from "pluralize";
14
- import { MySqlTable } from "drizzle-orm/mysql-core";
15
15
  import { SQLiteTable } from "drizzle-orm/sqlite-core";
16
16
  import SchemaBuilder, { BasePlugin } from "@pothos/core";
17
17
  import DrizzlePlugin from "@pothos/plugin-drizzle";
18
18
  import SmartSubscriptionsPlugin, { subscribeOptionsFromIterator } from "@pothos/plugin-smart-subscriptions";
19
- import TracingPlugin, { isRootField } from "@pothos/plugin-tracing";
20
- import { DateResolver, DateTimeISOResolver, JSONResolver } from "graphql-scalars";
19
+ import TracingPlugin, { isRootField, wrapResolver } from "@pothos/plugin-tracing";
20
+ import { BigIntResolver, ByteResolver, DateResolver, DateTimeISOResolver, JSONResolver } from "graphql-scalars";
21
21
  //#region lib/types/rumbleError.ts
22
22
  /**
23
23
  * An error that gets raised by rumble whenever something does not go according to plan.
@@ -180,7 +180,7 @@ function mapNullFieldsToUndefined(obj) {
180
180
  }
181
181
  //#endregion
182
182
  //#region package.json
183
- var version = "0.22.2";
183
+ var version = "0.23.2";
184
184
  //#endregion
185
185
  //#region lib/helpers/mergeFilters.ts
186
186
  function mergeFilters(filterA, filterB, mode = "AND") {
@@ -199,23 +199,47 @@ function mergeFilters(filterA, filterB, mode = "AND") {
199
199
  }
200
200
  //#endregion
201
201
  //#region lib/helpers/sqlTypes/types.ts
202
+ /**
203
+ * Strips parameter suffixes like `(256)`, `(10,2)` and timezone modifiers
204
+ * (`with time zone`, `without time zone`) from raw getSQLType() output so
205
+ * every classifier works on the base type name only.
206
+ */
207
+ function normalizeSQLType(raw) {
208
+ return raw.replace(/\s*with(?:out)?\s+time\s+zone/i, "").replace(/\s*\([^)]*\)/, "").trim().toLowerCase();
209
+ }
202
210
  const intLikeSQLTypeStrings = [
203
211
  "serial",
204
212
  "int",
205
213
  "integer",
206
214
  "tinyint",
207
215
  "smallint",
208
- "mediumint"
216
+ "mediumint",
217
+ "int2",
218
+ "int4",
219
+ "smallserial"
209
220
  ];
210
221
  function isIntLikeSQLTypeString(sqlType) {
211
222
  return intLikeSQLTypeStrings.includes(sqlType);
212
223
  }
224
+ const bigIntLikeSQLTypeStrings = [
225
+ "bigint",
226
+ "bigserial",
227
+ "int8",
228
+ "bigint unsigned"
229
+ ];
230
+ function isBigIntLikeSQLTypeString(sqlType) {
231
+ return bigIntLikeSQLTypeStrings.includes(sqlType);
232
+ }
213
233
  const floatLikeSQLTypeStrings = [
214
234
  "real",
215
235
  "decimal",
216
236
  "double",
217
237
  "float",
218
- "numeric"
238
+ "numeric",
239
+ "double precision",
240
+ "float4",
241
+ "float8",
242
+ "money"
219
243
  ];
220
244
  function isFloatLikeSQLTypeString(sqlType) {
221
245
  return floatLikeSQLTypeStrings.includes(sqlType);
@@ -225,7 +249,20 @@ const stringLikeSQLTypeStrings = [
225
249
  "text",
226
250
  "varchar",
227
251
  "char",
228
- "text(256)"
252
+ "character varying",
253
+ "character",
254
+ "inet",
255
+ "cidr",
256
+ "macaddr",
257
+ "macaddr8",
258
+ "interval",
259
+ "time",
260
+ "year",
261
+ "binary",
262
+ "varbinary",
263
+ "tinytext",
264
+ "mediumtext",
265
+ "longtext"
229
266
  ];
230
267
  function isStringLikeSQLTypeString(sqlType) {
231
268
  return stringLikeSQLTypeStrings.includes(sqlType);
@@ -234,7 +271,7 @@ const IDLikeSQLTypeStrings = ["uuid"];
234
271
  function isIDLikeSQLTypeString(sqlType) {
235
272
  return IDLikeSQLTypeStrings.includes(sqlType);
236
273
  }
237
- const booleanSQLTypeStrings = ["boolean"];
274
+ const booleanSQLTypeStrings = ["boolean", "bool"];
238
275
  function isBooleanSQLTypeString(sqlType) {
239
276
  return booleanSQLTypeStrings.includes(sqlType);
240
277
  }
@@ -250,16 +287,23 @@ const jsonLikeSQLTypeStrings = ["json", "jsonb"];
250
287
  function isJSONLikeSQLTypeString(sqlType) {
251
288
  return jsonLikeSQLTypeStrings.includes(sqlType);
252
289
  }
253
- [
254
- ...intLikeSQLTypeStrings,
255
- ...floatLikeSQLTypeStrings,
256
- ...stringLikeSQLTypeStrings,
257
- ...IDLikeSQLTypeStrings,
258
- ...booleanSQLTypeStrings,
259
- ...dateTimeLikeSQLTypeStrings,
260
- ...dateLikeSQLTypeStrings,
261
- ...jsonLikeSQLTypeStrings
290
+ const jsonFallbackSQLTypeStrings = [
291
+ "point",
292
+ "line",
293
+ "lseg",
294
+ "box",
295
+ "path",
296
+ "polygon",
297
+ "circle",
298
+ "geometry"
262
299
  ];
300
+ function isJSONFallbackSQLTypeString(sqlType) {
301
+ return jsonFallbackSQLTypeStrings.includes(sqlType) || sqlType.startsWith("geometry");
302
+ }
303
+ const bytesSQLTypeStrings = ["bytea", "blob"];
304
+ function isBytesSQLTypeString(sqlType) {
305
+ return bytesSQLTypeStrings.includes(sqlType);
306
+ }
263
307
  const UnknownTypeRumbleError = (sqlType, additionalInfo) => new RumbleError(`RumbleError: Unknown SQL type '${sqlType}'. Please open an issue (https://github.com/m1212e/rumble/issues) so it can be added. (${additionalInfo})`);
264
308
  //#endregion
265
309
  //#region lib/helpers/sqlTypes/distinctValuesFromSQLType.ts
@@ -374,16 +418,17 @@ function isDynamicQueryFilter(filter) {
374
418
  function isStaticQueryFilter(filter) {
375
419
  return typeof filter !== "function";
376
420
  }
377
- const nothingRegisteredWarningLogger = debounce((model, action) => {
378
- console.warn(`
379
- Warning! No abilities have been registered for
380
-
381
- ${model}/${action}
382
-
383
- but has been accessed. This will block everything. If this is intended, you can ignore this warning. If not, please ensure that you register the ability in your ability builder.
384
- `);
421
+ const makeNothingRegisteredWarner = (logger) => debounce((model, action) => {
422
+ const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
423
+ if (logger) logger.warn({
424
+ "rumble.table": model,
425
+ "rumble.action": action
426
+ }, msg);
427
+ else console.warn(msg);
385
428
  }, 1e3);
386
- const createAbilityBuilder = ({ db, actions, defaultLimit, otel }) => {
429
+ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
430
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
431
+ const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(log);
387
432
  let hasBeenBuilt = false;
388
433
  const createBuilderForTable = () => {
389
434
  const queryFilters = /* @__PURE__ */ new Map();
@@ -631,7 +676,8 @@ by: (explicitFilter) => {
631
676
  * Merges the current query filters with the provided filters for this call only
632
677
  */
633
678
  function merge(p) {
634
- return internalTransformer(mergeFilters(ret.query.many, p), p.limit);
679
+ const merged = mergeFilters(ret.query.many, p);
680
+ return internalTransformer(merged, p.limit);
635
681
  }
636
682
  ret.merge = merge;
637
683
  return ret;
@@ -673,14 +719,39 @@ by: (explicitFilter) => {
673
719
  span?.setAttribute("abilities.status", "applied");
674
720
  return transformToResponse(mergedFilters);
675
721
  };
676
- if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`prepare_query_abilities_${action}`, (span) => {
722
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`rumble.abilities.prepare`, (span) => {
723
+ span.setAttribute("rumble.action", action);
724
+ span.setAttribute("rumble.table", tableName.toString());
677
725
  try {
678
- return assembleAbilities(span);
726
+ const attrs = {
727
+ "rumble.table": tableName.toString(),
728
+ "rumble.action": action
729
+ };
730
+ const proxy = new Proxy(span, { get(target, prop) {
731
+ if (prop === "setAttribute") return (k, v) => {
732
+ attrs[k] = v;
733
+ return target.setAttribute(k, v);
734
+ };
735
+ return target[prop];
736
+ } });
737
+ const result = assembleAbilities(proxy);
738
+ log?.debug(attrs, "abilities prepared");
739
+ return result;
679
740
  } finally {
680
741
  span.end();
681
742
  }
682
743
  });
683
- else return assembleAbilities();
744
+ else {
745
+ const attrs = {
746
+ "rumble.table": tableName.toString(),
747
+ "rumble.action": action
748
+ };
749
+ const result = assembleAbilities(log ? { setAttribute: (k, v) => {
750
+ attrs[k] = v;
751
+ } } : void 0);
752
+ log?.debug(attrs, "abilities prepared");
753
+ return result;
754
+ }
684
755
  } };
685
756
  } };
686
757
  };
@@ -709,29 +780,14 @@ const createOrderArgImplementer = ({ db, schemaBuilder }) => {
709
780
  if (ret) return ret;
710
781
  const implement = () => {
711
782
  return schemaBuilder.inputType(inputTypeName, { fields: (t) => {
712
- const fields = Object.entries(tableSchema.columns).reduce((acc, [key]) => {
783
+ return Object.entries(tableSchema.columns).reduce((acc, [key]) => {
784
+ if (key.startsWith("__")) return acc;
713
785
  acc[key] = t.field({
714
786
  type: sortingParameterEnumRef(),
715
787
  required: false
716
788
  });
717
789
  return acc;
718
790
  }, {});
719
- const relations = Object.entries(tableSchema.relations ?? {}).reduce((acc, [key, value]) => {
720
- const targetTsName = value.targetTableName;
721
- const referenceModel = orderArgImplementer({ dbName: tableHelper({
722
- db,
723
- table: targetTsName
724
- }).dbName });
725
- acc[key] = t.field({
726
- type: referenceModel,
727
- required: false
728
- });
729
- return acc;
730
- }, {});
731
- return {
732
- ...fields,
733
- ...relations
734
- };
735
791
  } });
736
792
  };
737
793
  ret = implement();
@@ -743,10 +799,10 @@ const createOrderArgImplementer = ({ db, schemaBuilder }) => {
743
799
  //#endregion
744
800
  //#region lib/enum.ts
745
801
  /**
746
- * Checks if a column is a Postgres enum column
802
+ * Checks if a column is a database enum column (Postgres or MySQL).
747
803
  */
748
804
  function isEnumSchema(schemaType) {
749
- return is(schemaType, PgEnumColumn) || is(schemaType, PgEnumObjectColumn);
805
+ return is(schemaType, PgEnumColumn) || is(schemaType, PgEnumObjectColumn) || is(schemaType, MySqlEnumColumn) || is(schemaType, MySqlEnumObjectColumn);
750
806
  }
751
807
  const createEnumImplementer = ({ schema, schemaBuilder }) => {
752
808
  const referenceStorage = /* @__PURE__ */ new Map();
@@ -778,7 +834,12 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
778
834
  if (enumObject) {
779
835
  enumSchemaName = enumObject.enumName;
780
836
  enumValues = enumObject.enumValues;
781
- } else if (enumColumn) {
837
+ } else if (enumColumn) if (is(enumColumn, MySqlEnumColumn) || is(enumColumn, MySqlEnumObjectColumn)) {
838
+ enumValues = enumColumn.enumValues;
839
+ const colTable = enumColumn.table;
840
+ const tableName = colTable && isTable(colTable) ? getTableName(colTable) : void 0;
841
+ enumSchemaName = tableName ? `${tableName}_${enumColumn.name}` : enumColumn.name;
842
+ } else {
782
843
  const pgEnum = enumColumn.enum;
783
844
  if (!pgEnum) throw new RumbleError(`Could not find enum definition on column. Make sure the column is a pgEnum column.`);
784
845
  enumSchemaName = pgEnum.enumName;
@@ -797,16 +858,21 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
797
858
  //#endregion
798
859
  //#region lib/helpers/sqlTypes/mapSQLTypeToTSType.ts
799
860
  function mapSQLTypeToGraphQLType({ sqlType, fieldName }) {
861
+ if (/^enum\(/.test(sqlType)) return "String";
862
+ const normalized = normalizeSQLType(sqlType);
800
863
  let ret;
801
- if (isIntLikeSQLTypeString(sqlType)) ret = "Int";
802
- if (isFloatLikeSQLTypeString(sqlType)) ret = "Float";
803
- if (isStringLikeSQLTypeString(sqlType)) if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
864
+ if (isIntLikeSQLTypeString(normalized)) ret = "Int";
865
+ if (isBigIntLikeSQLTypeString(normalized)) ret = "BigInt";
866
+ if (isFloatLikeSQLTypeString(normalized)) ret = "Float";
867
+ if (isStringLikeSQLTypeString(normalized)) if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
804
868
  else ret = "String";
805
- if (isIDLikeSQLTypeString(sqlType)) ret = "ID";
806
- if (isBooleanSQLTypeString(sqlType)) ret = "Boolean";
807
- if (isDateTimeLikeSQLTypeString(sqlType)) ret = "DateTime";
808
- if (isDateLikeSQLTypeString(sqlType)) ret = "Date";
809
- if (isJSONLikeSQLTypeString(sqlType)) ret = "JSON";
869
+ if (isIDLikeSQLTypeString(normalized)) ret = "ID";
870
+ if (isBooleanSQLTypeString(normalized)) ret = "Boolean";
871
+ if (isDateTimeLikeSQLTypeString(normalized)) ret = "DateTime";
872
+ if (isDateLikeSQLTypeString(normalized)) ret = "Date";
873
+ if (isJSONLikeSQLTypeString(normalized)) ret = "JSON";
874
+ if (isJSONFallbackSQLTypeString(normalized)) ret = "JSON";
875
+ if (isBytesSQLTypeString(normalized)) ret = "Bytes";
810
876
  if (ret !== void 0) return ret;
811
877
  throw UnknownTypeRumbleError(sqlType, "SQL to GQL");
812
878
  }
@@ -816,6 +882,60 @@ const makeDefaultName = (dbName) => `${capitalize(toCamelCase(dbName.toString())
816
882
  const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
817
883
  const referenceStorage = /* @__PURE__ */ new Map();
818
884
  const enumArrayWhereInputStorage = /* @__PURE__ */ new Map();
885
+ const scalarArrayWhereInputStorage = /* @__PURE__ */ new Map();
886
+ const scalarArrayWhereInputName = (gqlType) => `${gqlType}ArrayWhereInputArgument`;
887
+ const getScalarArrayWhereInput = (gqlType) => {
888
+ const typeName = scalarArrayWhereInputName(gqlType);
889
+ const existing = scalarArrayWhereInputStorage.get(typeName);
890
+ if (existing) return existing;
891
+ const ret = schemaBuilder.inputRef(typeName);
892
+ scalarArrayWhereInputStorage.set(typeName, ret);
893
+ ret.implement({ fields: (t) => ({
894
+ eq: t.field({
895
+ type: [gqlType],
896
+ required: false
897
+ }),
898
+ ne: t.field({
899
+ type: [gqlType],
900
+ required: false
901
+ }),
902
+ in: t.field({
903
+ type: [[gqlType]],
904
+ required: false
905
+ }),
906
+ notIn: t.field({
907
+ type: [[gqlType]],
908
+ required: false
909
+ }),
910
+ isNull: t.boolean({ required: false }),
911
+ isNotNull: t.boolean({ required: false }),
912
+ arrayOverlaps: t.field({
913
+ type: [gqlType],
914
+ required: false
915
+ }),
916
+ arrayContained: t.field({
917
+ type: [gqlType],
918
+ required: false
919
+ }),
920
+ arrayContains: t.field({
921
+ type: [gqlType],
922
+ required: false
923
+ }),
924
+ AND: t.field({
925
+ type: [ret],
926
+ required: false
927
+ }),
928
+ OR: t.field({
929
+ type: [ret],
930
+ required: false
931
+ }),
932
+ NOT: t.field({
933
+ type: ret,
934
+ required: false
935
+ })
936
+ }) });
937
+ return ret;
938
+ };
819
939
  const getEnumArrayWhereInput = (enumImpl) => {
820
940
  const typeName = `${enumImpl.name}WhereInputArgument`;
821
941
  const existing = enumArrayWhereInputStorage.get(typeName);
@@ -879,39 +999,82 @@ const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
879
999
  const ret = schemaBuilder.inputRef(inputTypeName);
880
1000
  referenceStorage.set(inputTypeName, ret);
881
1001
  ret.implement({ fields: (t) => {
882
- const mapSQLTypeStringToInputPothosType = (sqlType, fieldName) => {
1002
+ const mapSQLTypeStringToInputPothosType = (sqlType, fieldName, isArray) => {
883
1003
  const gqlType = mapSQLTypeToGraphQLType({
884
1004
  sqlType,
885
1005
  fieldName
886
1006
  });
1007
+ if (isArray) return t.field({
1008
+ type: getScalarArrayWhereInput(gqlType),
1009
+ required: false
1010
+ });
887
1011
  switch (gqlType) {
888
- case "Int": return t.field({ type: "IntWhereInputArgument" });
889
- case "String": return t.field({ type: "StringWhereInputArgument" });
890
- case "Boolean": return t.field({ type: "BooleanWhereInputArgument" });
891
- case "Date": return t.field({ type: "DateWhereInputArgument" });
892
- case "DateTime": return t.field({ type: "DateWhereInputArgument" });
893
- case "Float": return t.field({ type: "FloatWhereInputArgument" });
894
- case "ID": return t.field({ type: "IDWhereInputArgument" });
895
- case "JSON": return t.field({ type: "JSONWhereInputArgument" });
1012
+ case "Int": return t.field({
1013
+ type: "IntWhereInputArgument",
1014
+ required: false
1015
+ });
1016
+ case "BigInt": return t.field({
1017
+ type: "BigIntWhereInputArgument",
1018
+ required: false
1019
+ });
1020
+ case "String": return t.field({
1021
+ type: "StringWhereInputArgument",
1022
+ required: false
1023
+ });
1024
+ case "Boolean": return t.field({
1025
+ type: "BooleanWhereInputArgument",
1026
+ required: false
1027
+ });
1028
+ case "Date": return t.field({
1029
+ type: "DateWhereInputArgument",
1030
+ required: false
1031
+ });
1032
+ case "DateTime": return t.field({
1033
+ type: "DateTimeWhereInputArgument",
1034
+ required: false
1035
+ });
1036
+ case "Float": return t.field({
1037
+ type: "FloatWhereInputArgument",
1038
+ required: false
1039
+ });
1040
+ case "ID": return t.field({
1041
+ type: "IDWhereInputArgument",
1042
+ required: false
1043
+ });
1044
+ case "JSON": return t.field({
1045
+ type: "JSONWhereInputArgument",
1046
+ required: false
1047
+ });
1048
+ case "Bytes": return null;
896
1049
  default: throw new RumbleError(`Unsupported argument type ${gqlType} for column ${sqlType}`);
897
1050
  }
898
1051
  };
899
1052
  const fields = Object.entries(tableSchema.columns).reduce((acc, [key, value]) => {
1053
+ if (key.startsWith("__")) return acc;
1054
+ let isArray = false;
1055
+ if (is(value, PgColumn)) {
1056
+ const dims = value.dimensions;
1057
+ if (dims && dims > 0) isArray = true;
1058
+ }
900
1059
  if (isEnumSchema(value)) {
901
1060
  const enumImpl = enumImplementer({ enumColumn: value });
902
1061
  acc[key] = t.field({
903
- type: value.dimensions > 0 ? getEnumArrayWhereInput(enumImpl) : enumImpl,
1062
+ type: isArray ? getEnumArrayWhereInput(enumImpl) : enumImpl,
904
1063
  required: false
905
1064
  });
906
- } else acc[key] = mapSQLTypeStringToInputPothosType(value.getSQLType(), key);
1065
+ } else {
1066
+ const field = mapSQLTypeStringToInputPothosType(value.getSQLType(), key, isArray);
1067
+ if (field !== null) acc[key] = field;
1068
+ }
907
1069
  return acc;
908
1070
  }, {});
909
1071
  const relations = Object.entries(tableSchema.relations ?? {}).reduce((acc, [key, value]) => {
910
1072
  const targetTsName = value.targetTableName;
911
- const referenceModel = whereArgImplementer({ dbName: tableHelper({
1073
+ const relationSchema = tableHelper({
912
1074
  db,
913
1075
  table: targetTsName
914
- }).dbName });
1076
+ });
1077
+ const referenceModel = whereArgImplementer({ dbName: relationSchema.dbName });
915
1078
  acc[key] = t.field({
916
1079
  type: referenceModel,
917
1080
  required: false
@@ -1117,6 +1280,11 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1117
1280
  });
1118
1281
  switch (gqlType) {
1119
1282
  case "Int": return isArray ? builder.exposeIntList(fieldName, { nullable }) : builder.exposeInt(fieldName, { nullable });
1283
+ case "BigInt": return builder.field({
1284
+ type: isArray ? ["BigInt"] : "BigInt",
1285
+ resolve: (element) => element[fieldName],
1286
+ nullable
1287
+ });
1120
1288
  case "String": return isArray ? builder.exposeStringList(fieldName, { nullable }) : builder.exposeString(fieldName, { nullable });
1121
1289
  case "Boolean": return isArray ? builder.exposeBooleanList(fieldName, { nullable }) : builder.exposeBoolean(fieldName, { nullable });
1122
1290
  case "Date": return builder.field({
@@ -1126,7 +1294,11 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1126
1294
  });
1127
1295
  case "DateTime": return builder.field({
1128
1296
  type: isArray ? ["DateTime"] : "DateTime",
1129
- resolve: (element) => element[fieldName],
1297
+ resolve: (element) => {
1298
+ const v = element[fieldName];
1299
+ if (v == null) return v;
1300
+ return v instanceof Date ? v : new Date(v);
1301
+ },
1130
1302
  nullable
1131
1303
  });
1132
1304
  case "Float": return isArray ? builder.exposeFloatList(fieldName, { nullable }) : builder.exposeFloat(fieldName, { nullable });
@@ -1136,6 +1308,15 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1136
1308
  resolve: (element) => element[fieldName],
1137
1309
  nullable
1138
1310
  });
1311
+ case "Bytes": return builder.field({
1312
+ type: "Bytes",
1313
+ resolve: (element) => {
1314
+ const v = element[fieldName];
1315
+ if (v == null) return v;
1316
+ return Buffer.isBuffer(v) ? v.toString("base64") : String(v);
1317
+ },
1318
+ nullable
1319
+ });
1139
1320
  default: throw new RumbleError(`Unsupported object type ${gqlType} for column ${fieldName}`);
1140
1321
  }
1141
1322
  }
@@ -1167,14 +1348,17 @@ function isPostgresDB(db) {
1167
1348
  //#endregion
1168
1349
  //#region lib/search.ts
1169
1350
  async function initSearchIfApplicable(input) {
1351
+ const log = input.logger?.enabled ? input.logger.logger : void 0;
1170
1352
  if (!isPostgresDB(input.db)) {
1171
- console.info("Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.");
1353
+ const msg = "Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.";
1354
+ log ? log.info({}, msg) : console.info(msg);
1172
1355
  return;
1173
1356
  }
1174
1357
  try {
1175
1358
  await input.db.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm;`);
1176
1359
  } catch (error) {
1177
- console.error("Failed to create pg_trgm extension. Search functionality may not work. Ensure the database user has CREATE privilege or the extension is pre-installed.", error);
1360
+ const msg = "Failed to create pg_trgm extension. Search functionality may not work. Ensure the database user has CREATE privilege or the extension is pre-installed.";
1361
+ log ? log.error({ err: error }, msg) : console.error(msg, error);
1178
1362
  return;
1179
1363
  }
1180
1364
  if (input.search?.threshold) {
@@ -1233,13 +1417,17 @@ const isProbablyAConfigObject = (t) => {
1233
1417
  ].find((e) => e === k))) return true;
1234
1418
  return false;
1235
1419
  };
1236
- const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1420
+ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1237
1421
  return ({ table, refName, readAction = "read", adjust }) => {
1238
1422
  const tableSchema = tableHelper({
1239
1423
  db,
1240
1424
  table
1241
1425
  });
1242
- if (Object.keys(tableSchema.primaryKey).length === 0) console.warn(`Could not find primary key for ${table.toString()}. Cannot register subscriptions!`);
1426
+ if (Object.keys(tableSchema.primaryKey).length === 0) {
1427
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1428
+ const msg = `Could not find primary key for ${table.toString()}. Cannot register subscriptions!`;
1429
+ log ? log.warn({ "rumble.table": table.toString() }, msg) : console.warn(msg);
1430
+ }
1243
1431
  const primaryKey = Object.values(tableSchema.primaryKey)[0];
1244
1432
  const { registerOnInstance } = makePubSubInstance({ table });
1245
1433
  return schemaBuilder.drizzleObject(table, {
@@ -1248,7 +1436,9 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
1248
1436
  if (!primaryKey) return;
1249
1437
  const primaryKeyValue = element[primaryKey.name];
1250
1438
  if (!primaryKeyValue) {
1251
- console.warn(`Could not find primary key value for ${JSON.stringify(element)}. Cannot register subscription!`);
1439
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1440
+ const msg = `Could not find primary key value for element on ${table.toString()}. Cannot register subscription!`;
1441
+ log ? log.warn({ "rumble.table": table.toString() }, msg) : console.warn(msg);
1252
1442
  return;
1253
1443
  }
1254
1444
  registerOnInstance({
@@ -1279,6 +1469,10 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
1279
1469
  };
1280
1470
  } })) ?? {};
1281
1471
  const fields = Object.entries(columns).reduce((acc, [key, column]) => {
1472
+ if (key.startsWith("__")) {
1473
+ console.warn(`Skipping column "${key}" on table "${tableSchema.tsName}": names starting with "__" are reserved by GraphQL introspection. Use a custom adjust() call if you really need to expose this column.`);
1474
+ return acc;
1475
+ }
1282
1476
  if (userAdjustments[key]) {
1283
1477
  const { params, creatorFunction, configObject } = configMap.get(userAdjustments[key]);
1284
1478
  if (typeof configObject.nullable !== "boolean") configObject.nullable = !column.notNull;
@@ -1479,7 +1673,8 @@ const createPubSubInstance = ({ subscriptions }) => {
1479
1673
  };
1480
1674
  //#endregion
1481
1675
  //#region lib/query.ts
1482
- const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1676
+ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1677
+ const registeredQueryFieldNames = /* @__PURE__ */ new Set();
1483
1678
  return ({ table, readAction = "read", listAction = "read" }) => {
1484
1679
  const WhereArg = whereArgImplementer({ table });
1485
1680
  const OrderArg = orderArgImplementer({ table });
@@ -1489,6 +1684,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1489
1684
  });
1490
1685
  const primaryKeyField = Object.values(tableSchema.primaryKey)[0];
1491
1686
  const { registerOnInstance } = makePubSubInstance({ table });
1687
+ const pluralName = pluralize.plural(table.toString());
1688
+ const singularName = pluralize.singular(table.toString());
1689
+ for (const name of [pluralName, singularName]) {
1690
+ if (registeredQueryFieldNames.has(name)) throw new RumbleError(`Duplicate query field "${name}": the table "${table.toString()}" generates a query field that collides with one already registered. Use unique table names (or refNames) to avoid plural/singular collisions (e.g. "post" and "posts").`);
1691
+ registeredQueryFieldNames.add(name);
1692
+ }
1492
1693
  return schemaBuilder.queryFields((t) => {
1493
1694
  const manyArgs = {
1494
1695
  where: t.arg({
@@ -1505,11 +1706,11 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1505
1706
  };
1506
1707
  if (!search?.enabled) delete manyArgs.search;
1507
1708
  return {
1508
- [pluralize.plural(table.toString())]: t.drizzleField({
1709
+ [pluralName]: t.drizzleField({
1509
1710
  type: [table],
1510
1711
  nullable: false,
1511
1712
  smartSubscription: true,
1512
- description: `List all ${pluralize.plural(table.toString())}`,
1713
+ description: `List all ${pluralName}`,
1513
1714
  subscribe: (subscriptions, _root, _args, _ctx, _info) => {
1514
1715
  registerOnInstance({
1515
1716
  instance: subscriptions,
@@ -1537,17 +1738,21 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1537
1738
  if (filter.columns) queryInstance.columns = filter.columns;
1538
1739
  if (search?.enabled && args.search && args.search.length > 0 && search?.cpu_operator_cost) return db.transaction(async (tx) => {
1539
1740
  if (isPostgresDB(tx)) await tx.execute(sql.raw(`SET cpu_operator_cost = ${search.cpu_operator_cost};`));
1540
- else console.info("Database dialect is not postgresql, cannot set cpu_operator_cost.");
1741
+ else {
1742
+ const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
1743
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1744
+ log ? log.info({}, msg) : console.info(msg);
1745
+ }
1541
1746
  return tx.query[table].findMany(queryInstance);
1542
1747
  });
1543
1748
  return db.query[table].findMany(queryInstance);
1544
1749
  }
1545
1750
  }),
1546
- [pluralize.singular(table.toString())]: t.drizzleField({
1751
+ [singularName]: t.drizzleField({
1547
1752
  type: table,
1548
1753
  nullable: false,
1549
1754
  smartSubscription: true,
1550
- description: `Get a single ${pluralize.singular(table.toString())} by ID`,
1755
+ description: `Get a single ${singularName} by ID`,
1551
1756
  args: { id: t.arg.id({ required: true }) },
1552
1757
  resolve: (query, _root, args, ctx, _info) => {
1553
1758
  deepSetProto(args);
@@ -1723,6 +1928,96 @@ function implementDefaultWhereInputArgs(schemaBuilder) {
1723
1928
  OR: t.field({ type: [JSONWhereInputArgument] }),
1724
1929
  NOT: t.field({ type: JSONWhereInputArgument })
1725
1930
  }) });
1931
+ const BigIntWhereInputArgument = schemaBuilder.inputRef("BigIntWhereInputArgument").implement({ fields: (t) => ({
1932
+ eq: t.field({
1933
+ type: "BigInt",
1934
+ required: false
1935
+ }),
1936
+ ne: t.field({
1937
+ type: "BigInt",
1938
+ required: false
1939
+ }),
1940
+ gt: t.field({
1941
+ type: "BigInt",
1942
+ required: false
1943
+ }),
1944
+ gte: t.field({
1945
+ type: "BigInt",
1946
+ required: false
1947
+ }),
1948
+ lt: t.field({
1949
+ type: "BigInt",
1950
+ required: false
1951
+ }),
1952
+ lte: t.field({
1953
+ type: "BigInt",
1954
+ required: false
1955
+ }),
1956
+ in: t.field({
1957
+ type: ["BigInt"],
1958
+ required: false
1959
+ }),
1960
+ notIn: t.field({
1961
+ type: ["BigInt"],
1962
+ required: false
1963
+ }),
1964
+ isNull: t.boolean(),
1965
+ isNotNull: t.boolean(),
1966
+ AND: t.field({ type: [BigIntWhereInputArgument] }),
1967
+ OR: t.field({ type: [BigIntWhereInputArgument] }),
1968
+ NOT: t.field({ type: BigIntWhereInputArgument })
1969
+ }) });
1970
+ const DateTimeWhereInputArgument = schemaBuilder.inputRef("DateTimeWhereInputArgument").implement({ fields: (t) => ({
1971
+ eq: t.field({
1972
+ type: "DateTime",
1973
+ required: false
1974
+ }),
1975
+ ne: t.field({
1976
+ type: "DateTime",
1977
+ required: false
1978
+ }),
1979
+ gt: t.field({
1980
+ type: "DateTime",
1981
+ required: false
1982
+ }),
1983
+ gte: t.field({
1984
+ type: "DateTime",
1985
+ required: false
1986
+ }),
1987
+ lt: t.field({
1988
+ type: "DateTime",
1989
+ required: false
1990
+ }),
1991
+ lte: t.field({
1992
+ type: "DateTime",
1993
+ required: false
1994
+ }),
1995
+ in: t.field({
1996
+ type: ["DateTime"],
1997
+ required: false
1998
+ }),
1999
+ notIn: t.field({
2000
+ type: ["DateTime"],
2001
+ required: false
2002
+ }),
2003
+ isNull: t.boolean(),
2004
+ isNotNull: t.boolean(),
2005
+ arrayOverlaps: t.field({
2006
+ type: ["DateTime"],
2007
+ required: false
2008
+ }),
2009
+ arrayContained: t.field({
2010
+ type: ["DateTime"],
2011
+ required: false
2012
+ }),
2013
+ arrayContains: t.field({
2014
+ type: ["DateTime"],
2015
+ required: false
2016
+ }),
2017
+ AND: t.field({ type: [DateTimeWhereInputArgument] }),
2018
+ OR: t.field({ type: [DateTimeWhereInputArgument] }),
2019
+ NOT: t.field({ type: DateTimeWhereInputArgument })
2020
+ }) });
1726
2021
  }
1727
2022
  //#endregion
1728
2023
  //#region lib/runtimeFiltersPlugin/filterTypes.ts
@@ -1733,9 +2028,11 @@ const applyFiltersKey = "applyFilters";
1733
2028
  var RuntimeFiltersPlugin = class extends BasePlugin {
1734
2029
  tracer;
1735
2030
  tracerEnabled;
2031
+ logger;
1736
2032
  onTypeConfig(typeConfig) {
1737
2033
  this.tracer = this.builder.options.otel?.tracer;
1738
2034
  this.tracerEnabled = this.builder.options.otel?.enabled;
2035
+ this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
1739
2036
  return typeConfig;
1740
2037
  }
1741
2038
  wrapResolve(resolver, fieldConfig) {
@@ -1764,7 +2061,8 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
1764
2061
  }));
1765
2062
  let resolved, prefetchedFilters;
1766
2063
  if (this.tracer && this.tracerEnabled) {
1767
- const o = await this.tracer.startActiveSpan(`apply_filters`, async (span) => {
2064
+ const o = await this.tracer.startActiveSpan(`rumble.filter.resolve`, async (span) => {
2065
+ span.setAttribute("graphql.field.name", fieldConfig.name);
1768
2066
  try {
1769
2067
  return await Promise.all([resolver(parent, args, context, info), prefetchedFiltersPromises]);
1770
2068
  } finally {
@@ -1786,17 +2084,37 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
1786
2084
  return acc;
1787
2085
  }, /* @__PURE__ */ new Set()));
1788
2086
  span?.setAttribute("filters.allowed", allowed.length);
2087
+ this.logger?.debug({
2088
+ "graphql.field.name": fieldConfig.name,
2089
+ "filters.total": allFilters.length,
2090
+ "filters.allowed": allowed.length
2091
+ }, "runtime filters applied");
1789
2092
  if (Array.isArray(resolved)) return allowed;
1790
2093
  return allowed[0] ?? null;
1791
2094
  };
1792
- if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`filter_${fieldConfig.name}`, async (span) => {
2095
+ if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`rumble.filter.apply`, async (span) => {
2096
+ span.setAttribute("graphql.field.name", fieldConfig.name);
1793
2097
  try {
1794
2098
  return await runFilters(span);
2099
+ } catch (error) {
2100
+ this.logger?.error({
2101
+ "graphql.field.name": fieldConfig.name,
2102
+ err: error
2103
+ }, "runtime filter threw");
2104
+ throw error;
1795
2105
  } finally {
1796
2106
  span.end();
1797
2107
  }
1798
2108
  });
1799
- else return runFilters();
2109
+ else try {
2110
+ return await runFilters();
2111
+ } catch (error) {
2112
+ this.logger?.error({
2113
+ "graphql.field.name": fieldConfig.name,
2114
+ err: error
2115
+ }, "runtime filter threw");
2116
+ throw error;
2117
+ }
1800
2118
  };
1801
2119
  }
1802
2120
  };
@@ -1809,7 +2127,7 @@ function registerRuntimeFiltersPlugin() {
1809
2127
  }
1810
2128
  //#endregion
1811
2129
  //#region lib/schemaBuilder.ts
1812
- const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig, otel }) => {
2130
+ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
1813
2131
  const createSpan = otel?.enabled && otel.tracer ? createOpenTelemetryWrapper(otel.tracer, otel.options) : void 0;
1814
2132
  registerRuntimeFiltersPlugin();
1815
2133
  const schemaBuilder = new SchemaBuilder({
@@ -1837,18 +2155,40 @@ const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig,
1837
2155
  }) },
1838
2156
  defaultFieldNullability: false,
1839
2157
  tracing: {
1840
- default: otel?.enabled ? (config) => isRootField(config) : () => false,
1841
- wrap: createSpan ? (resolver, options) => createSpan(resolver, options) : (resolver) => resolver
2158
+ default: otel?.enabled || logger?.enabled ? (config) => isRootField(config) : () => false,
2159
+ wrap: (resolver, options, config) => {
2160
+ let r = createSpan ? createSpan(resolver, options) : resolver;
2161
+ if (logger?.enabled) {
2162
+ const log = logger.logger;
2163
+ r = wrapResolver(r, (error, duration) => {
2164
+ if (error) log.error({
2165
+ "graphql.field.name": config.name,
2166
+ "graphql.parent.type": config.parentType,
2167
+ durationMs: duration,
2168
+ err: error
2169
+ }, "resolver failed");
2170
+ else log.debug({
2171
+ "graphql.field.name": config.name,
2172
+ "graphql.parent.type": config.parentType,
2173
+ durationMs: duration
2174
+ }, "resolver completed");
2175
+ });
2176
+ }
2177
+ return r;
2178
+ }
1842
2179
  },
1843
- otel
2180
+ otel,
2181
+ logger
1844
2182
  });
1845
2183
  schemaBuilder.addScalarType("JSON", JSONResolver);
1846
2184
  schemaBuilder.addScalarType("Date", DateResolver);
1847
2185
  schemaBuilder.addScalarType("DateTime", DateTimeISOResolver);
2186
+ schemaBuilder.addScalarType("BigInt", BigIntResolver);
2187
+ schemaBuilder.addScalarType("Bytes", ByteResolver);
1848
2188
  implementDefaultWhereInputArgs(schemaBuilder);
1849
- if (!disableDefaultObjects?.query) schemaBuilder.queryType({});
1850
- if (!disableDefaultObjects?.subscription) schemaBuilder.subscriptionType({});
1851
- if (!disableDefaultObjects?.mutation) schemaBuilder.mutationType({});
2189
+ schemaBuilder.queryType({});
2190
+ schemaBuilder.subscriptionType({});
2191
+ schemaBuilder.mutationType({});
1852
2192
  return { schemaBuilder };
1853
2193
  };
1854
2194
  //#endregion
@@ -1931,7 +2271,33 @@ export const r = rumble({
1931
2271
  whereArgImplementer: whereArg,
1932
2272
  makePubSubInstance
1933
2273
  });
1934
- const builtSchema = lazy(() => schemaBuilder.toSchema());
2274
+ const builtSchema = lazy(() => {
2275
+ let raw;
2276
+ try {
2277
+ raw = schemaBuilder.toSchema();
2278
+ } catch (e) {
2279
+ const unimplMatch = (e?.message ?? String(e)).match(/ObjectRef<(.+?)> has not been implemented/);
2280
+ if (unimplMatch) throw new RumbleError(`Could not build GraphQL schema: the table "${unimplMatch[1]}" is referenced in a relation but has not been registered. Call rumble.object({ table: "${unimplMatch[1]}" }) before building the schema.`);
2281
+ throw e;
2282
+ }
2283
+ const mutation = raw.getMutationType();
2284
+ const subscription = raw.getSubscriptionType();
2285
+ const emptyMutation = mutation && Object.keys(mutation.getFields()).length === 0;
2286
+ const emptySubscription = subscription && Object.keys(subscription.getFields()).length === 0;
2287
+ if (!emptyMutation && !emptySubscription) return raw;
2288
+ const config = raw.toConfig();
2289
+ const filteredTypes = config.types.filter((t) => {
2290
+ if (emptyMutation && t === mutation) return false;
2291
+ if (emptySubscription && t === subscription) return false;
2292
+ return true;
2293
+ });
2294
+ return new GraphQLSchema({
2295
+ ...config,
2296
+ mutation: emptyMutation ? void 0 : config.mutation,
2297
+ subscription: emptySubscription ? void 0 : config.subscription,
2298
+ types: filteredTypes
2299
+ });
2300
+ });
1935
2301
  const createYoga$1 = (args) => {
1936
2302
  const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
1937
2303
  return createYoga({
@@ -1942,26 +2308,60 @@ export const r = rumble({
1942
2308
  plugins: [
1943
2309
  ...args?.plugins ?? [],
1944
2310
  ...enableApiDocs ? [] : [useDisableIntrospection(), EnvelopArmorPlugin(args?.armorConfig)],
1945
- rumbleInput.otel?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
1946
- setExecuteFn((options) => rumbleInput.otel.tracer.startActiveSpan(SpanNames.EXECUTE, { attributes: {
1947
- [AttributeNames.OPERATION_NAME]: options.operationName ?? "anonymous",
1948
- [AttributeNames.SOURCE]: options.document
1949
- } }, async (span) => {
1950
- try {
2311
+ rumbleInput.otel?.enabled || rumbleInput.logger?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
2312
+ setExecuteFn(async (options) => {
2313
+ const log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
2314
+ const operationName = options.operationName ?? "anonymous";
2315
+ const start = Date.now();
2316
+ log?.info({ "graphql.operation.name": operationName }, "graphql execute start");
2317
+ const run = async () => {
1951
2318
  const result = await executeFn(options);
1952
- if (result && "errors" in result && result.errors?.length) {
1953
- for (const error of result.errors) span.recordException(error);
2319
+ if (result && "errors" in result && result.errors?.length) log?.error({
2320
+ "graphql.operation.name": operationName,
2321
+ durationMs: Date.now() - start,
2322
+ errors: result.errors.map((e) => e.message)
2323
+ }, "graphql execute completed with errors");
2324
+ else log?.info({
2325
+ "graphql.operation.name": operationName,
2326
+ durationMs: Date.now() - start
2327
+ }, "graphql execute completed");
2328
+ return result;
2329
+ };
2330
+ if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(SpanNames.EXECUTE, { attributes: {
2331
+ [AttributeNames.OPERATION_NAME]: operationName,
2332
+ [AttributeNames.SOURCE]: options.document
2333
+ } }, async (span) => {
2334
+ try {
2335
+ const result = await run();
2336
+ if (result && "errors" in result && result.errors?.length) {
2337
+ for (const error of result.errors) span.recordException(error);
2338
+ span.setStatus({ code: SpanStatusCode.ERROR });
2339
+ }
2340
+ return result;
2341
+ } catch (error) {
2342
+ if (error instanceof Error) span.recordException(error);
2343
+ log?.error({
2344
+ "graphql.operation.name": operationName,
2345
+ durationMs: Date.now() - start,
2346
+ err: error
2347
+ }, "graphql execute threw");
1954
2348
  span.setStatus({ code: SpanStatusCode.ERROR });
2349
+ throw error;
2350
+ } finally {
2351
+ span.end();
1955
2352
  }
1956
- return result;
2353
+ });
2354
+ try {
2355
+ return await run();
1957
2356
  } catch (error) {
1958
- if (error instanceof Error) span.recordException(error);
1959
- span.setStatus({ code: SpanStatusCode.ERROR });
2357
+ log?.error({
2358
+ "graphql.operation.name": operationName,
2359
+ durationMs: Date.now() - start,
2360
+ err: error
2361
+ }, "graphql execute threw");
1960
2362
  throw error;
1961
- } finally {
1962
- span.end();
1963
2363
  }
1964
- }));
2364
+ });
1965
2365
  } } : false
1966
2366
  ].filter(Boolean)
1967
2367
  });