@m1212e/rumble 0.22.1 → 0.23.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
@@ -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.1";
183
+ var version = "0.23.0";
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();
@@ -673,14 +718,38 @@ by: (explicitFilter) => {
673
718
  span?.setAttribute("abilities.status", "applied");
674
719
  return transformToResponse(mergedFilters);
675
720
  };
676
- if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`prepare_query_abilities_${action}`, (span) => {
721
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`rumble.abilities.prepare`, (span) => {
722
+ span.setAttribute("rumble.action", action);
723
+ span.setAttribute("rumble.table", tableName.toString());
677
724
  try {
678
- return assembleAbilities(span);
725
+ const attrs = {
726
+ "rumble.table": tableName.toString(),
727
+ "rumble.action": action
728
+ };
729
+ const result = assembleAbilities(new Proxy(span, { get(target, prop) {
730
+ if (prop === "setAttribute") return (k, v) => {
731
+ attrs[k] = v;
732
+ return target.setAttribute(k, v);
733
+ };
734
+ return target[prop];
735
+ } }));
736
+ log?.debug(attrs, "abilities prepared");
737
+ return result;
679
738
  } finally {
680
739
  span.end();
681
740
  }
682
741
  });
683
- else return assembleAbilities();
742
+ else {
743
+ const attrs = {
744
+ "rumble.table": tableName.toString(),
745
+ "rumble.action": action
746
+ };
747
+ const result = assembleAbilities(log ? { setAttribute: (k, v) => {
748
+ attrs[k] = v;
749
+ } } : void 0);
750
+ log?.debug(attrs, "abilities prepared");
751
+ return result;
752
+ }
684
753
  } };
685
754
  } };
686
755
  };
@@ -709,29 +778,14 @@ const createOrderArgImplementer = ({ db, schemaBuilder }) => {
709
778
  if (ret) return ret;
710
779
  const implement = () => {
711
780
  return schemaBuilder.inputType(inputTypeName, { fields: (t) => {
712
- const fields = Object.entries(tableSchema.columns).reduce((acc, [key]) => {
781
+ return Object.entries(tableSchema.columns).reduce((acc, [key]) => {
782
+ if (key.startsWith("__")) return acc;
713
783
  acc[key] = t.field({
714
784
  type: sortingParameterEnumRef(),
715
785
  required: false
716
786
  });
717
787
  return acc;
718
788
  }, {});
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
789
  } });
736
790
  };
737
791
  ret = implement();
@@ -743,10 +797,10 @@ const createOrderArgImplementer = ({ db, schemaBuilder }) => {
743
797
  //#endregion
744
798
  //#region lib/enum.ts
745
799
  /**
746
- * Checks if a column is a Postgres enum column
800
+ * Checks if a column is a database enum column (Postgres or MySQL).
747
801
  */
748
802
  function isEnumSchema(schemaType) {
749
- return is(schemaType, PgEnumColumn) || is(schemaType, PgEnumObjectColumn);
803
+ return is(schemaType, PgEnumColumn) || is(schemaType, PgEnumObjectColumn) || is(schemaType, MySqlEnumColumn) || is(schemaType, MySqlEnumObjectColumn);
750
804
  }
751
805
  const createEnumImplementer = ({ schema, schemaBuilder }) => {
752
806
  const referenceStorage = /* @__PURE__ */ new Map();
@@ -778,7 +832,12 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
778
832
  if (enumObject) {
779
833
  enumSchemaName = enumObject.enumName;
780
834
  enumValues = enumObject.enumValues;
781
- } else if (enumColumn) {
835
+ } else if (enumColumn) if (is(enumColumn, MySqlEnumColumn) || is(enumColumn, MySqlEnumObjectColumn)) {
836
+ enumValues = enumColumn.enumValues;
837
+ const colTable = enumColumn.table;
838
+ const tableName = colTable && isTable(colTable) ? getTableName(colTable) : void 0;
839
+ enumSchemaName = tableName ? `${tableName}_${enumColumn.name}` : enumColumn.name;
840
+ } else {
782
841
  const pgEnum = enumColumn.enum;
783
842
  if (!pgEnum) throw new RumbleError(`Could not find enum definition on column. Make sure the column is a pgEnum column.`);
784
843
  enumSchemaName = pgEnum.enumName;
@@ -797,16 +856,21 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
797
856
  //#endregion
798
857
  //#region lib/helpers/sqlTypes/mapSQLTypeToTSType.ts
799
858
  function mapSQLTypeToGraphQLType({ sqlType, fieldName }) {
859
+ if (/^enum\(/.test(sqlType)) return "String";
860
+ const normalized = normalizeSQLType(sqlType);
800
861
  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";
862
+ if (isIntLikeSQLTypeString(normalized)) ret = "Int";
863
+ if (isBigIntLikeSQLTypeString(normalized)) ret = "BigInt";
864
+ if (isFloatLikeSQLTypeString(normalized)) ret = "Float";
865
+ if (isStringLikeSQLTypeString(normalized)) if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
804
866
  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";
867
+ if (isIDLikeSQLTypeString(normalized)) ret = "ID";
868
+ if (isBooleanSQLTypeString(normalized)) ret = "Boolean";
869
+ if (isDateTimeLikeSQLTypeString(normalized)) ret = "DateTime";
870
+ if (isDateLikeSQLTypeString(normalized)) ret = "Date";
871
+ if (isJSONLikeSQLTypeString(normalized)) ret = "JSON";
872
+ if (isJSONFallbackSQLTypeString(normalized)) ret = "JSON";
873
+ if (isBytesSQLTypeString(normalized)) ret = "Bytes";
810
874
  if (ret !== void 0) return ret;
811
875
  throw UnknownTypeRumbleError(sqlType, "SQL to GQL");
812
876
  }
@@ -816,6 +880,60 @@ const makeDefaultName = (dbName) => `${capitalize(toCamelCase(dbName.toString())
816
880
  const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
817
881
  const referenceStorage = /* @__PURE__ */ new Map();
818
882
  const enumArrayWhereInputStorage = /* @__PURE__ */ new Map();
883
+ const scalarArrayWhereInputStorage = /* @__PURE__ */ new Map();
884
+ const scalarArrayWhereInputName = (gqlType) => `${gqlType}ArrayWhereInputArgument`;
885
+ const getScalarArrayWhereInput = (gqlType) => {
886
+ const typeName = scalarArrayWhereInputName(gqlType);
887
+ const existing = scalarArrayWhereInputStorage.get(typeName);
888
+ if (existing) return existing;
889
+ const ret = schemaBuilder.inputRef(typeName);
890
+ scalarArrayWhereInputStorage.set(typeName, ret);
891
+ ret.implement({ fields: (t) => ({
892
+ eq: t.field({
893
+ type: [gqlType],
894
+ required: false
895
+ }),
896
+ ne: t.field({
897
+ type: [gqlType],
898
+ required: false
899
+ }),
900
+ in: t.field({
901
+ type: [[gqlType]],
902
+ required: false
903
+ }),
904
+ notIn: t.field({
905
+ type: [[gqlType]],
906
+ required: false
907
+ }),
908
+ isNull: t.boolean({ required: false }),
909
+ isNotNull: t.boolean({ required: false }),
910
+ arrayOverlaps: t.field({
911
+ type: [gqlType],
912
+ required: false
913
+ }),
914
+ arrayContained: t.field({
915
+ type: [gqlType],
916
+ required: false
917
+ }),
918
+ arrayContains: t.field({
919
+ type: [gqlType],
920
+ required: false
921
+ }),
922
+ AND: t.field({
923
+ type: [ret],
924
+ required: false
925
+ }),
926
+ OR: t.field({
927
+ type: [ret],
928
+ required: false
929
+ }),
930
+ NOT: t.field({
931
+ type: ret,
932
+ required: false
933
+ })
934
+ }) });
935
+ return ret;
936
+ };
819
937
  const getEnumArrayWhereInput = (enumImpl) => {
820
938
  const typeName = `${enumImpl.name}WhereInputArgument`;
821
939
  const existing = enumArrayWhereInputStorage.get(typeName);
@@ -824,19 +942,19 @@ const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
824
942
  enumArrayWhereInputStorage.set(typeName, ret);
825
943
  ret.implement({ fields: (t) => ({
826
944
  eq: t.field({
827
- type: enumImpl,
945
+ type: [enumImpl],
828
946
  required: false
829
947
  }),
830
948
  ne: t.field({
831
- type: enumImpl,
949
+ type: [enumImpl],
832
950
  required: false
833
951
  }),
834
952
  in: t.field({
835
- type: [enumImpl],
953
+ type: [[enumImpl]],
836
954
  required: false
837
955
  }),
838
956
  notIn: t.field({
839
- type: [enumImpl],
957
+ type: [[enumImpl]],
840
958
  required: false
841
959
  }),
842
960
  isNull: t.boolean({ required: false }),
@@ -879,31 +997,73 @@ const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
879
997
  const ret = schemaBuilder.inputRef(inputTypeName);
880
998
  referenceStorage.set(inputTypeName, ret);
881
999
  ret.implement({ fields: (t) => {
882
- const mapSQLTypeStringToInputPothosType = (sqlType, fieldName) => {
1000
+ const mapSQLTypeStringToInputPothosType = (sqlType, fieldName, isArray) => {
883
1001
  const gqlType = mapSQLTypeToGraphQLType({
884
1002
  sqlType,
885
1003
  fieldName
886
1004
  });
1005
+ if (isArray) return t.field({
1006
+ type: getScalarArrayWhereInput(gqlType),
1007
+ required: false
1008
+ });
887
1009
  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" });
1010
+ case "Int": return t.field({
1011
+ type: "IntWhereInputArgument",
1012
+ required: false
1013
+ });
1014
+ case "BigInt": return t.field({
1015
+ type: "BigIntWhereInputArgument",
1016
+ required: false
1017
+ });
1018
+ case "String": return t.field({
1019
+ type: "StringWhereInputArgument",
1020
+ required: false
1021
+ });
1022
+ case "Boolean": return t.field({
1023
+ type: "BooleanWhereInputArgument",
1024
+ required: false
1025
+ });
1026
+ case "Date": return t.field({
1027
+ type: "DateWhereInputArgument",
1028
+ required: false
1029
+ });
1030
+ case "DateTime": return t.field({
1031
+ type: "DateTimeWhereInputArgument",
1032
+ required: false
1033
+ });
1034
+ case "Float": return t.field({
1035
+ type: "FloatWhereInputArgument",
1036
+ required: false
1037
+ });
1038
+ case "ID": return t.field({
1039
+ type: "IDWhereInputArgument",
1040
+ required: false
1041
+ });
1042
+ case "JSON": return t.field({
1043
+ type: "JSONWhereInputArgument",
1044
+ required: false
1045
+ });
1046
+ case "Bytes": return null;
896
1047
  default: throw new RumbleError(`Unsupported argument type ${gqlType} for column ${sqlType}`);
897
1048
  }
898
1049
  };
899
1050
  const fields = Object.entries(tableSchema.columns).reduce((acc, [key, value]) => {
1051
+ if (key.startsWith("__")) return acc;
1052
+ let isArray = false;
1053
+ if (is(value, PgColumn)) {
1054
+ const dims = value.dimensions;
1055
+ if (dims && dims > 0) isArray = true;
1056
+ }
900
1057
  if (isEnumSchema(value)) {
901
1058
  const enumImpl = enumImplementer({ enumColumn: value });
902
1059
  acc[key] = t.field({
903
- type: value.dimensions > 0 ? getEnumArrayWhereInput(enumImpl) : enumImpl,
1060
+ type: isArray ? getEnumArrayWhereInput(enumImpl) : enumImpl,
904
1061
  required: false
905
1062
  });
906
- } else acc[key] = mapSQLTypeStringToInputPothosType(value.getSQLType(), key);
1063
+ } else {
1064
+ const field = mapSQLTypeStringToInputPothosType(value.getSQLType(), key, isArray);
1065
+ if (field !== null) acc[key] = field;
1066
+ }
907
1067
  return acc;
908
1068
  }, {});
909
1069
  const relations = Object.entries(tableSchema.relations ?? {}).reduce((acc, [key, value]) => {
@@ -1117,6 +1277,11 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1117
1277
  });
1118
1278
  switch (gqlType) {
1119
1279
  case "Int": return isArray ? builder.exposeIntList(fieldName, { nullable }) : builder.exposeInt(fieldName, { nullable });
1280
+ case "BigInt": return builder.field({
1281
+ type: isArray ? ["BigInt"] : "BigInt",
1282
+ resolve: (element) => element[fieldName],
1283
+ nullable
1284
+ });
1120
1285
  case "String": return isArray ? builder.exposeStringList(fieldName, { nullable }) : builder.exposeString(fieldName, { nullable });
1121
1286
  case "Boolean": return isArray ? builder.exposeBooleanList(fieldName, { nullable }) : builder.exposeBoolean(fieldName, { nullable });
1122
1287
  case "Date": return builder.field({
@@ -1126,7 +1291,11 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1126
1291
  });
1127
1292
  case "DateTime": return builder.field({
1128
1293
  type: isArray ? ["DateTime"] : "DateTime",
1129
- resolve: (element) => element[fieldName],
1294
+ resolve: (element) => {
1295
+ const v = element[fieldName];
1296
+ if (v == null) return v;
1297
+ return v instanceof Date ? v : new Date(v);
1298
+ },
1130
1299
  nullable
1131
1300
  });
1132
1301
  case "Float": return isArray ? builder.exposeFloatList(fieldName, { nullable }) : builder.exposeFloat(fieldName, { nullable });
@@ -1136,6 +1305,15 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1136
1305
  resolve: (element) => element[fieldName],
1137
1306
  nullable
1138
1307
  });
1308
+ case "Bytes": return builder.field({
1309
+ type: "Bytes",
1310
+ resolve: (element) => {
1311
+ const v = element[fieldName];
1312
+ if (v == null) return v;
1313
+ return Buffer.isBuffer(v) ? v.toString("base64") : String(v);
1314
+ },
1315
+ nullable
1316
+ });
1139
1317
  default: throw new RumbleError(`Unsupported object type ${gqlType} for column ${fieldName}`);
1140
1318
  }
1141
1319
  }
@@ -1167,14 +1345,17 @@ function isPostgresDB(db) {
1167
1345
  //#endregion
1168
1346
  //#region lib/search.ts
1169
1347
  async function initSearchIfApplicable(input) {
1348
+ const log = input.logger?.enabled ? input.logger.logger : void 0;
1170
1349
  if (!isPostgresDB(input.db)) {
1171
- console.info("Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.");
1350
+ const msg = "Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.";
1351
+ log ? log.info({}, msg) : console.info(msg);
1172
1352
  return;
1173
1353
  }
1174
1354
  try {
1175
1355
  await input.db.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm;`);
1176
1356
  } 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);
1357
+ 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.";
1358
+ log ? log.error({ err: error }, msg) : console.error(msg, error);
1178
1359
  return;
1179
1360
  }
1180
1361
  if (input.search?.threshold) {
@@ -1233,13 +1414,17 @@ const isProbablyAConfigObject = (t) => {
1233
1414
  ].find((e) => e === k))) return true;
1234
1415
  return false;
1235
1416
  };
1236
- const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1417
+ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1237
1418
  return ({ table, refName, readAction = "read", adjust }) => {
1238
1419
  const tableSchema = tableHelper({
1239
1420
  db,
1240
1421
  table
1241
1422
  });
1242
- if (Object.keys(tableSchema.primaryKey).length === 0) console.warn(`Could not find primary key for ${table.toString()}. Cannot register subscriptions!`);
1423
+ if (Object.keys(tableSchema.primaryKey).length === 0) {
1424
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1425
+ const msg = `Could not find primary key for ${table.toString()}. Cannot register subscriptions!`;
1426
+ log ? log.warn({ "rumble.table": table.toString() }, msg) : console.warn(msg);
1427
+ }
1243
1428
  const primaryKey = Object.values(tableSchema.primaryKey)[0];
1244
1429
  const { registerOnInstance } = makePubSubInstance({ table });
1245
1430
  return schemaBuilder.drizzleObject(table, {
@@ -1248,7 +1433,9 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
1248
1433
  if (!primaryKey) return;
1249
1434
  const primaryKeyValue = element[primaryKey.name];
1250
1435
  if (!primaryKeyValue) {
1251
- console.warn(`Could not find primary key value for ${JSON.stringify(element)}. Cannot register subscription!`);
1436
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1437
+ const msg = `Could not find primary key value for element on ${table.toString()}. Cannot register subscription!`;
1438
+ log ? log.warn({ "rumble.table": table.toString() }, msg) : console.warn(msg);
1252
1439
  return;
1253
1440
  }
1254
1441
  registerOnInstance({
@@ -1279,6 +1466,10 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
1279
1466
  };
1280
1467
  } })) ?? {};
1281
1468
  const fields = Object.entries(columns).reduce((acc, [key, column]) => {
1469
+ if (key.startsWith("__")) {
1470
+ 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.`);
1471
+ return acc;
1472
+ }
1282
1473
  if (userAdjustments[key]) {
1283
1474
  const { params, creatorFunction, configObject } = configMap.get(userAdjustments[key]);
1284
1475
  if (typeof configObject.nullable !== "boolean") configObject.nullable = !column.notNull;
@@ -1479,7 +1670,8 @@ const createPubSubInstance = ({ subscriptions }) => {
1479
1670
  };
1480
1671
  //#endregion
1481
1672
  //#region lib/query.ts
1482
- const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1673
+ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1674
+ const registeredQueryFieldNames = /* @__PURE__ */ new Set();
1483
1675
  return ({ table, readAction = "read", listAction = "read" }) => {
1484
1676
  const WhereArg = whereArgImplementer({ table });
1485
1677
  const OrderArg = orderArgImplementer({ table });
@@ -1489,6 +1681,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1489
1681
  });
1490
1682
  const primaryKeyField = Object.values(tableSchema.primaryKey)[0];
1491
1683
  const { registerOnInstance } = makePubSubInstance({ table });
1684
+ const pluralName = pluralize.plural(table.toString());
1685
+ const singularName = pluralize.singular(table.toString());
1686
+ for (const name of [pluralName, singularName]) {
1687
+ 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").`);
1688
+ registeredQueryFieldNames.add(name);
1689
+ }
1492
1690
  return schemaBuilder.queryFields((t) => {
1493
1691
  const manyArgs = {
1494
1692
  where: t.arg({
@@ -1505,11 +1703,11 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1505
1703
  };
1506
1704
  if (!search?.enabled) delete manyArgs.search;
1507
1705
  return {
1508
- [pluralize.plural(table.toString())]: t.drizzleField({
1706
+ [pluralName]: t.drizzleField({
1509
1707
  type: [table],
1510
1708
  nullable: false,
1511
1709
  smartSubscription: true,
1512
- description: `List all ${pluralize.plural(table.toString())}`,
1710
+ description: `List all ${pluralName}`,
1513
1711
  subscribe: (subscriptions, _root, _args, _ctx, _info) => {
1514
1712
  registerOnInstance({
1515
1713
  instance: subscriptions,
@@ -1537,17 +1735,21 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1537
1735
  if (filter.columns) queryInstance.columns = filter.columns;
1538
1736
  if (search?.enabled && args.search && args.search.length > 0 && search?.cpu_operator_cost) return db.transaction(async (tx) => {
1539
1737
  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.");
1738
+ else {
1739
+ const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
1740
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1741
+ log ? log.info({}, msg) : console.info(msg);
1742
+ }
1541
1743
  return tx.query[table].findMany(queryInstance);
1542
1744
  });
1543
1745
  return db.query[table].findMany(queryInstance);
1544
1746
  }
1545
1747
  }),
1546
- [pluralize.singular(table.toString())]: t.drizzleField({
1748
+ [singularName]: t.drizzleField({
1547
1749
  type: table,
1548
1750
  nullable: false,
1549
1751
  smartSubscription: true,
1550
- description: `Get a single ${pluralize.singular(table.toString())} by ID`,
1752
+ description: `Get a single ${singularName} by ID`,
1551
1753
  args: { id: t.arg.id({ required: true }) },
1552
1754
  resolve: (query, _root, args, ctx, _info) => {
1553
1755
  deepSetProto(args);
@@ -1723,6 +1925,96 @@ function implementDefaultWhereInputArgs(schemaBuilder) {
1723
1925
  OR: t.field({ type: [JSONWhereInputArgument] }),
1724
1926
  NOT: t.field({ type: JSONWhereInputArgument })
1725
1927
  }) });
1928
+ const BigIntWhereInputArgument = schemaBuilder.inputRef("BigIntWhereInputArgument").implement({ fields: (t) => ({
1929
+ eq: t.field({
1930
+ type: "BigInt",
1931
+ required: false
1932
+ }),
1933
+ ne: t.field({
1934
+ type: "BigInt",
1935
+ required: false
1936
+ }),
1937
+ gt: t.field({
1938
+ type: "BigInt",
1939
+ required: false
1940
+ }),
1941
+ gte: t.field({
1942
+ type: "BigInt",
1943
+ required: false
1944
+ }),
1945
+ lt: t.field({
1946
+ type: "BigInt",
1947
+ required: false
1948
+ }),
1949
+ lte: t.field({
1950
+ type: "BigInt",
1951
+ required: false
1952
+ }),
1953
+ in: t.field({
1954
+ type: ["BigInt"],
1955
+ required: false
1956
+ }),
1957
+ notIn: t.field({
1958
+ type: ["BigInt"],
1959
+ required: false
1960
+ }),
1961
+ isNull: t.boolean(),
1962
+ isNotNull: t.boolean(),
1963
+ AND: t.field({ type: [BigIntWhereInputArgument] }),
1964
+ OR: t.field({ type: [BigIntWhereInputArgument] }),
1965
+ NOT: t.field({ type: BigIntWhereInputArgument })
1966
+ }) });
1967
+ const DateTimeWhereInputArgument = schemaBuilder.inputRef("DateTimeWhereInputArgument").implement({ fields: (t) => ({
1968
+ eq: t.field({
1969
+ type: "DateTime",
1970
+ required: false
1971
+ }),
1972
+ ne: t.field({
1973
+ type: "DateTime",
1974
+ required: false
1975
+ }),
1976
+ gt: t.field({
1977
+ type: "DateTime",
1978
+ required: false
1979
+ }),
1980
+ gte: t.field({
1981
+ type: "DateTime",
1982
+ required: false
1983
+ }),
1984
+ lt: t.field({
1985
+ type: "DateTime",
1986
+ required: false
1987
+ }),
1988
+ lte: t.field({
1989
+ type: "DateTime",
1990
+ required: false
1991
+ }),
1992
+ in: t.field({
1993
+ type: ["DateTime"],
1994
+ required: false
1995
+ }),
1996
+ notIn: t.field({
1997
+ type: ["DateTime"],
1998
+ required: false
1999
+ }),
2000
+ isNull: t.boolean(),
2001
+ isNotNull: t.boolean(),
2002
+ arrayOverlaps: t.field({
2003
+ type: ["DateTime"],
2004
+ required: false
2005
+ }),
2006
+ arrayContained: t.field({
2007
+ type: ["DateTime"],
2008
+ required: false
2009
+ }),
2010
+ arrayContains: t.field({
2011
+ type: ["DateTime"],
2012
+ required: false
2013
+ }),
2014
+ AND: t.field({ type: [DateTimeWhereInputArgument] }),
2015
+ OR: t.field({ type: [DateTimeWhereInputArgument] }),
2016
+ NOT: t.field({ type: DateTimeWhereInputArgument })
2017
+ }) });
1726
2018
  }
1727
2019
  //#endregion
1728
2020
  //#region lib/runtimeFiltersPlugin/filterTypes.ts
@@ -1733,9 +2025,11 @@ const applyFiltersKey = "applyFilters";
1733
2025
  var RuntimeFiltersPlugin = class extends BasePlugin {
1734
2026
  tracer;
1735
2027
  tracerEnabled;
2028
+ logger;
1736
2029
  onTypeConfig(typeConfig) {
1737
2030
  this.tracer = this.builder.options.otel?.tracer;
1738
2031
  this.tracerEnabled = this.builder.options.otel?.enabled;
2032
+ this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
1739
2033
  return typeConfig;
1740
2034
  }
1741
2035
  wrapResolve(resolver, fieldConfig) {
@@ -1764,7 +2058,8 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
1764
2058
  }));
1765
2059
  let resolved, prefetchedFilters;
1766
2060
  if (this.tracer && this.tracerEnabled) {
1767
- const o = await this.tracer.startActiveSpan(`apply_filters`, async (span) => {
2061
+ const o = await this.tracer.startActiveSpan(`rumble.filter.resolve`, async (span) => {
2062
+ span.setAttribute("graphql.field.name", fieldConfig.name);
1768
2063
  try {
1769
2064
  return await Promise.all([resolver(parent, args, context, info), prefetchedFiltersPromises]);
1770
2065
  } finally {
@@ -1786,17 +2081,37 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
1786
2081
  return acc;
1787
2082
  }, /* @__PURE__ */ new Set()));
1788
2083
  span?.setAttribute("filters.allowed", allowed.length);
2084
+ this.logger?.debug({
2085
+ "graphql.field.name": fieldConfig.name,
2086
+ "filters.total": allFilters.length,
2087
+ "filters.allowed": allowed.length
2088
+ }, "runtime filters applied");
1789
2089
  if (Array.isArray(resolved)) return allowed;
1790
2090
  return allowed[0] ?? null;
1791
2091
  };
1792
- if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`filter_${fieldConfig.name}`, async (span) => {
2092
+ if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`rumble.filter.apply`, async (span) => {
2093
+ span.setAttribute("graphql.field.name", fieldConfig.name);
1793
2094
  try {
1794
2095
  return await runFilters(span);
2096
+ } catch (error) {
2097
+ this.logger?.error({
2098
+ "graphql.field.name": fieldConfig.name,
2099
+ err: error
2100
+ }, "runtime filter threw");
2101
+ throw error;
1795
2102
  } finally {
1796
2103
  span.end();
1797
2104
  }
1798
2105
  });
1799
- else return runFilters();
2106
+ else try {
2107
+ return await runFilters();
2108
+ } catch (error) {
2109
+ this.logger?.error({
2110
+ "graphql.field.name": fieldConfig.name,
2111
+ err: error
2112
+ }, "runtime filter threw");
2113
+ throw error;
2114
+ }
1800
2115
  };
1801
2116
  }
1802
2117
  };
@@ -1809,7 +2124,7 @@ function registerRuntimeFiltersPlugin() {
1809
2124
  }
1810
2125
  //#endregion
1811
2126
  //#region lib/schemaBuilder.ts
1812
- const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig, otel }) => {
2127
+ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
1813
2128
  const createSpan = otel?.enabled && otel.tracer ? createOpenTelemetryWrapper(otel.tracer, otel.options) : void 0;
1814
2129
  registerRuntimeFiltersPlugin();
1815
2130
  const schemaBuilder = new SchemaBuilder({
@@ -1837,18 +2152,40 @@ const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig,
1837
2152
  }) },
1838
2153
  defaultFieldNullability: false,
1839
2154
  tracing: {
1840
- default: otel?.enabled ? (config) => isRootField(config) : () => false,
1841
- wrap: createSpan ? (resolver, options) => createSpan(resolver, options) : (resolver) => resolver
2155
+ default: otel?.enabled || logger?.enabled ? (config) => isRootField(config) : () => false,
2156
+ wrap: (resolver, options, config) => {
2157
+ let r = createSpan ? createSpan(resolver, options) : resolver;
2158
+ if (logger?.enabled) {
2159
+ const log = logger.logger;
2160
+ r = wrapResolver(r, (error, duration) => {
2161
+ if (error) log.error({
2162
+ "graphql.field.name": config.name,
2163
+ "graphql.parent.type": config.parentType,
2164
+ durationMs: duration,
2165
+ err: error
2166
+ }, "resolver failed");
2167
+ else log.debug({
2168
+ "graphql.field.name": config.name,
2169
+ "graphql.parent.type": config.parentType,
2170
+ durationMs: duration
2171
+ }, "resolver completed");
2172
+ });
2173
+ }
2174
+ return r;
2175
+ }
1842
2176
  },
1843
- otel
2177
+ otel,
2178
+ logger
1844
2179
  });
1845
2180
  schemaBuilder.addScalarType("JSON", JSONResolver);
1846
2181
  schemaBuilder.addScalarType("Date", DateResolver);
1847
2182
  schemaBuilder.addScalarType("DateTime", DateTimeISOResolver);
2183
+ schemaBuilder.addScalarType("BigInt", BigIntResolver);
2184
+ schemaBuilder.addScalarType("Bytes", ByteResolver);
1848
2185
  implementDefaultWhereInputArgs(schemaBuilder);
1849
- if (!disableDefaultObjects?.query) schemaBuilder.queryType({});
1850
- if (!disableDefaultObjects?.subscription) schemaBuilder.subscriptionType({});
1851
- if (!disableDefaultObjects?.mutation) schemaBuilder.mutationType({});
2186
+ schemaBuilder.queryType({});
2187
+ schemaBuilder.subscriptionType({});
2188
+ schemaBuilder.mutationType({});
1852
2189
  return { schemaBuilder };
1853
2190
  };
1854
2191
  //#endregion
@@ -1931,7 +2268,33 @@ export const r = rumble({
1931
2268
  whereArgImplementer: whereArg,
1932
2269
  makePubSubInstance
1933
2270
  });
1934
- const builtSchema = lazy(() => schemaBuilder.toSchema());
2271
+ const builtSchema = lazy(() => {
2272
+ let raw;
2273
+ try {
2274
+ raw = schemaBuilder.toSchema();
2275
+ } catch (e) {
2276
+ const unimplMatch = (e?.message ?? String(e)).match(/ObjectRef<(.+?)> has not been implemented/);
2277
+ 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.`);
2278
+ throw e;
2279
+ }
2280
+ const mutation = raw.getMutationType();
2281
+ const subscription = raw.getSubscriptionType();
2282
+ const emptyMutation = mutation && Object.keys(mutation.getFields()).length === 0;
2283
+ const emptySubscription = subscription && Object.keys(subscription.getFields()).length === 0;
2284
+ if (!emptyMutation && !emptySubscription) return raw;
2285
+ const config = raw.toConfig();
2286
+ const filteredTypes = config.types.filter((t) => {
2287
+ if (emptyMutation && t === mutation) return false;
2288
+ if (emptySubscription && t === subscription) return false;
2289
+ return true;
2290
+ });
2291
+ return new GraphQLSchema({
2292
+ ...config,
2293
+ mutation: emptyMutation ? void 0 : config.mutation,
2294
+ subscription: emptySubscription ? void 0 : config.subscription,
2295
+ types: filteredTypes
2296
+ });
2297
+ });
1935
2298
  const createYoga$1 = (args) => {
1936
2299
  const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
1937
2300
  return createYoga({
@@ -1942,26 +2305,60 @@ export const r = rumble({
1942
2305
  plugins: [
1943
2306
  ...args?.plugins ?? [],
1944
2307
  ...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 {
2308
+ rumbleInput.otel?.enabled || rumbleInput.logger?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
2309
+ setExecuteFn(async (options) => {
2310
+ const log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
2311
+ const operationName = options.operationName ?? "anonymous";
2312
+ const start = Date.now();
2313
+ log?.info({ "graphql.operation.name": operationName }, "graphql execute start");
2314
+ const run = async () => {
1951
2315
  const result = await executeFn(options);
1952
- if (result && "errors" in result && result.errors?.length) {
1953
- for (const error of result.errors) span.recordException(error);
2316
+ if (result && "errors" in result && result.errors?.length) log?.error({
2317
+ "graphql.operation.name": operationName,
2318
+ durationMs: Date.now() - start,
2319
+ errors: result.errors.map((e) => e.message)
2320
+ }, "graphql execute completed with errors");
2321
+ else log?.info({
2322
+ "graphql.operation.name": operationName,
2323
+ durationMs: Date.now() - start
2324
+ }, "graphql execute completed");
2325
+ return result;
2326
+ };
2327
+ if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(SpanNames.EXECUTE, { attributes: {
2328
+ [AttributeNames.OPERATION_NAME]: operationName,
2329
+ [AttributeNames.SOURCE]: options.document
2330
+ } }, async (span) => {
2331
+ try {
2332
+ const result = await run();
2333
+ if (result && "errors" in result && result.errors?.length) {
2334
+ for (const error of result.errors) span.recordException(error);
2335
+ span.setStatus({ code: SpanStatusCode.ERROR });
2336
+ }
2337
+ return result;
2338
+ } catch (error) {
2339
+ if (error instanceof Error) span.recordException(error);
2340
+ log?.error({
2341
+ "graphql.operation.name": operationName,
2342
+ durationMs: Date.now() - start,
2343
+ err: error
2344
+ }, "graphql execute threw");
1954
2345
  span.setStatus({ code: SpanStatusCode.ERROR });
2346
+ throw error;
2347
+ } finally {
2348
+ span.end();
1955
2349
  }
1956
- return result;
2350
+ });
2351
+ try {
2352
+ return await run();
1957
2353
  } catch (error) {
1958
- if (error instanceof Error) span.recordException(error);
1959
- span.setStatus({ code: SpanStatusCode.ERROR });
2354
+ log?.error({
2355
+ "graphql.operation.name": operationName,
2356
+ durationMs: Date.now() - start,
2357
+ err: error
2358
+ }, "graphql execute threw");
1960
2359
  throw error;
1961
- } finally {
1962
- span.end();
1963
2360
  }
1964
- }));
2361
+ });
1965
2362
  } } : false
1966
2363
  ].filter(Boolean)
1967
2364
  });