@m1212e/rumble 0.22.2 → 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.cjs CHANGED
@@ -33,9 +33,9 @@ let graphql_yoga = require("graphql-yoga");
33
33
  let drizzle_orm = require("drizzle-orm");
34
34
  let drizzle_orm_casing = require("drizzle-orm/casing");
35
35
  let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
36
+ let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
36
37
  let pluralize = require("pluralize");
37
38
  pluralize = __toESM(pluralize, 1);
38
- let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
39
39
  let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
40
40
  let _pothos_core = require("@pothos/core");
41
41
  _pothos_core = __toESM(_pothos_core, 1);
@@ -208,7 +208,7 @@ function mapNullFieldsToUndefined(obj) {
208
208
  }
209
209
  //#endregion
210
210
  //#region package.json
211
- var version = "0.22.2";
211
+ var version = "0.23.0";
212
212
  //#endregion
213
213
  //#region lib/helpers/mergeFilters.ts
214
214
  function mergeFilters(filterA, filterB, mode = "AND") {
@@ -227,23 +227,47 @@ function mergeFilters(filterA, filterB, mode = "AND") {
227
227
  }
228
228
  //#endregion
229
229
  //#region lib/helpers/sqlTypes/types.ts
230
+ /**
231
+ * Strips parameter suffixes like `(256)`, `(10,2)` and timezone modifiers
232
+ * (`with time zone`, `without time zone`) from raw getSQLType() output so
233
+ * every classifier works on the base type name only.
234
+ */
235
+ function normalizeSQLType(raw) {
236
+ return raw.replace(/\s*with(?:out)?\s+time\s+zone/i, "").replace(/\s*\([^)]*\)/, "").trim().toLowerCase();
237
+ }
230
238
  const intLikeSQLTypeStrings = [
231
239
  "serial",
232
240
  "int",
233
241
  "integer",
234
242
  "tinyint",
235
243
  "smallint",
236
- "mediumint"
244
+ "mediumint",
245
+ "int2",
246
+ "int4",
247
+ "smallserial"
237
248
  ];
238
249
  function isIntLikeSQLTypeString(sqlType) {
239
250
  return intLikeSQLTypeStrings.includes(sqlType);
240
251
  }
252
+ const bigIntLikeSQLTypeStrings = [
253
+ "bigint",
254
+ "bigserial",
255
+ "int8",
256
+ "bigint unsigned"
257
+ ];
258
+ function isBigIntLikeSQLTypeString(sqlType) {
259
+ return bigIntLikeSQLTypeStrings.includes(sqlType);
260
+ }
241
261
  const floatLikeSQLTypeStrings = [
242
262
  "real",
243
263
  "decimal",
244
264
  "double",
245
265
  "float",
246
- "numeric"
266
+ "numeric",
267
+ "double precision",
268
+ "float4",
269
+ "float8",
270
+ "money"
247
271
  ];
248
272
  function isFloatLikeSQLTypeString(sqlType) {
249
273
  return floatLikeSQLTypeStrings.includes(sqlType);
@@ -253,7 +277,20 @@ const stringLikeSQLTypeStrings = [
253
277
  "text",
254
278
  "varchar",
255
279
  "char",
256
- "text(256)"
280
+ "character varying",
281
+ "character",
282
+ "inet",
283
+ "cidr",
284
+ "macaddr",
285
+ "macaddr8",
286
+ "interval",
287
+ "time",
288
+ "year",
289
+ "binary",
290
+ "varbinary",
291
+ "tinytext",
292
+ "mediumtext",
293
+ "longtext"
257
294
  ];
258
295
  function isStringLikeSQLTypeString(sqlType) {
259
296
  return stringLikeSQLTypeStrings.includes(sqlType);
@@ -262,7 +299,7 @@ const IDLikeSQLTypeStrings = ["uuid"];
262
299
  function isIDLikeSQLTypeString(sqlType) {
263
300
  return IDLikeSQLTypeStrings.includes(sqlType);
264
301
  }
265
- const booleanSQLTypeStrings = ["boolean"];
302
+ const booleanSQLTypeStrings = ["boolean", "bool"];
266
303
  function isBooleanSQLTypeString(sqlType) {
267
304
  return booleanSQLTypeStrings.includes(sqlType);
268
305
  }
@@ -278,16 +315,23 @@ const jsonLikeSQLTypeStrings = ["json", "jsonb"];
278
315
  function isJSONLikeSQLTypeString(sqlType) {
279
316
  return jsonLikeSQLTypeStrings.includes(sqlType);
280
317
  }
281
- [
282
- ...intLikeSQLTypeStrings,
283
- ...floatLikeSQLTypeStrings,
284
- ...stringLikeSQLTypeStrings,
285
- ...IDLikeSQLTypeStrings,
286
- ...booleanSQLTypeStrings,
287
- ...dateTimeLikeSQLTypeStrings,
288
- ...dateLikeSQLTypeStrings,
289
- ...jsonLikeSQLTypeStrings
318
+ const jsonFallbackSQLTypeStrings = [
319
+ "point",
320
+ "line",
321
+ "lseg",
322
+ "box",
323
+ "path",
324
+ "polygon",
325
+ "circle",
326
+ "geometry"
290
327
  ];
328
+ function isJSONFallbackSQLTypeString(sqlType) {
329
+ return jsonFallbackSQLTypeStrings.includes(sqlType) || sqlType.startsWith("geometry");
330
+ }
331
+ const bytesSQLTypeStrings = ["bytea", "blob"];
332
+ function isBytesSQLTypeString(sqlType) {
333
+ return bytesSQLTypeStrings.includes(sqlType);
334
+ }
291
335
  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})`);
292
336
  //#endregion
293
337
  //#region lib/helpers/sqlTypes/distinctValuesFromSQLType.ts
@@ -402,16 +446,17 @@ function isDynamicQueryFilter(filter) {
402
446
  function isStaticQueryFilter(filter) {
403
447
  return typeof filter !== "function";
404
448
  }
405
- const nothingRegisteredWarningLogger = (0, es_toolkit.debounce)((model, action) => {
406
- console.warn(`
407
- Warning! No abilities have been registered for
408
-
409
- ${model}/${action}
410
-
411
- 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.
412
- `);
449
+ const makeNothingRegisteredWarner = (logger) => (0, es_toolkit.debounce)((model, action) => {
450
+ const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
451
+ if (logger) logger.warn({
452
+ "rumble.table": model,
453
+ "rumble.action": action
454
+ }, msg);
455
+ else console.warn(msg);
413
456
  }, 1e3);
414
- const createAbilityBuilder = ({ db, actions, defaultLimit, otel }) => {
457
+ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
458
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
459
+ const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(log);
415
460
  let hasBeenBuilt = false;
416
461
  const createBuilderForTable = () => {
417
462
  const queryFilters = /* @__PURE__ */ new Map();
@@ -701,14 +746,38 @@ by: (explicitFilter) => {
701
746
  span?.setAttribute("abilities.status", "applied");
702
747
  return transformToResponse(mergedFilters);
703
748
  };
704
- if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`prepare_query_abilities_${action}`, (span) => {
749
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`rumble.abilities.prepare`, (span) => {
750
+ span.setAttribute("rumble.action", action);
751
+ span.setAttribute("rumble.table", tableName.toString());
705
752
  try {
706
- return assembleAbilities(span);
753
+ const attrs = {
754
+ "rumble.table": tableName.toString(),
755
+ "rumble.action": action
756
+ };
757
+ const result = assembleAbilities(new Proxy(span, { get(target, prop) {
758
+ if (prop === "setAttribute") return (k, v) => {
759
+ attrs[k] = v;
760
+ return target.setAttribute(k, v);
761
+ };
762
+ return target[prop];
763
+ } }));
764
+ log?.debug(attrs, "abilities prepared");
765
+ return result;
707
766
  } finally {
708
767
  span.end();
709
768
  }
710
769
  });
711
- else return assembleAbilities();
770
+ else {
771
+ const attrs = {
772
+ "rumble.table": tableName.toString(),
773
+ "rumble.action": action
774
+ };
775
+ const result = assembleAbilities(log ? { setAttribute: (k, v) => {
776
+ attrs[k] = v;
777
+ } } : void 0);
778
+ log?.debug(attrs, "abilities prepared");
779
+ return result;
780
+ }
712
781
  } };
713
782
  } };
714
783
  };
@@ -737,29 +806,14 @@ const createOrderArgImplementer = ({ db, schemaBuilder }) => {
737
806
  if (ret) return ret;
738
807
  const implement = () => {
739
808
  return schemaBuilder.inputType(inputTypeName, { fields: (t) => {
740
- const fields = Object.entries(tableSchema.columns).reduce((acc, [key]) => {
809
+ return Object.entries(tableSchema.columns).reduce((acc, [key]) => {
810
+ if (key.startsWith("__")) return acc;
741
811
  acc[key] = t.field({
742
812
  type: sortingParameterEnumRef(),
743
813
  required: false
744
814
  });
745
815
  return acc;
746
816
  }, {});
747
- const relations = Object.entries(tableSchema.relations ?? {}).reduce((acc, [key, value]) => {
748
- const targetTsName = value.targetTableName;
749
- const referenceModel = orderArgImplementer({ dbName: tableHelper({
750
- db,
751
- table: targetTsName
752
- }).dbName });
753
- acc[key] = t.field({
754
- type: referenceModel,
755
- required: false
756
- });
757
- return acc;
758
- }, {});
759
- return {
760
- ...fields,
761
- ...relations
762
- };
763
817
  } });
764
818
  };
765
819
  ret = implement();
@@ -771,10 +825,10 @@ const createOrderArgImplementer = ({ db, schemaBuilder }) => {
771
825
  //#endregion
772
826
  //#region lib/enum.ts
773
827
  /**
774
- * Checks if a column is a Postgres enum column
828
+ * Checks if a column is a database enum column (Postgres or MySQL).
775
829
  */
776
830
  function isEnumSchema(schemaType) {
777
- return (0, drizzle_orm.is)(schemaType, drizzle_orm_pg_core.PgEnumColumn) || (0, drizzle_orm.is)(schemaType, drizzle_orm_pg_core.PgEnumObjectColumn);
831
+ return (0, drizzle_orm.is)(schemaType, drizzle_orm_pg_core.PgEnumColumn) || (0, drizzle_orm.is)(schemaType, drizzle_orm_pg_core.PgEnumObjectColumn) || (0, drizzle_orm.is)(schemaType, drizzle_orm_mysql_core.MySqlEnumColumn) || (0, drizzle_orm.is)(schemaType, drizzle_orm_mysql_core.MySqlEnumObjectColumn);
778
832
  }
779
833
  const createEnumImplementer = ({ schema, schemaBuilder }) => {
780
834
  const referenceStorage = /* @__PURE__ */ new Map();
@@ -806,7 +860,12 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
806
860
  if (enumObject) {
807
861
  enumSchemaName = enumObject.enumName;
808
862
  enumValues = enumObject.enumValues;
809
- } else if (enumColumn) {
863
+ } else if (enumColumn) if ((0, drizzle_orm.is)(enumColumn, drizzle_orm_mysql_core.MySqlEnumColumn) || (0, drizzle_orm.is)(enumColumn, drizzle_orm_mysql_core.MySqlEnumObjectColumn)) {
864
+ enumValues = enumColumn.enumValues;
865
+ const colTable = enumColumn.table;
866
+ const tableName = colTable && (0, drizzle_orm.isTable)(colTable) ? (0, drizzle_orm.getTableName)(colTable) : void 0;
867
+ enumSchemaName = tableName ? `${tableName}_${enumColumn.name}` : enumColumn.name;
868
+ } else {
810
869
  const pgEnum = enumColumn.enum;
811
870
  if (!pgEnum) throw new RumbleError(`Could not find enum definition on column. Make sure the column is a pgEnum column.`);
812
871
  enumSchemaName = pgEnum.enumName;
@@ -825,16 +884,21 @@ const createEnumImplementer = ({ schema, schemaBuilder }) => {
825
884
  //#endregion
826
885
  //#region lib/helpers/sqlTypes/mapSQLTypeToTSType.ts
827
886
  function mapSQLTypeToGraphQLType({ sqlType, fieldName }) {
887
+ if (/^enum\(/.test(sqlType)) return "String";
888
+ const normalized = normalizeSQLType(sqlType);
828
889
  let ret;
829
- if (isIntLikeSQLTypeString(sqlType)) ret = "Int";
830
- if (isFloatLikeSQLTypeString(sqlType)) ret = "Float";
831
- if (isStringLikeSQLTypeString(sqlType)) if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
890
+ if (isIntLikeSQLTypeString(normalized)) ret = "Int";
891
+ if (isBigIntLikeSQLTypeString(normalized)) ret = "BigInt";
892
+ if (isFloatLikeSQLTypeString(normalized)) ret = "Float";
893
+ if (isStringLikeSQLTypeString(normalized)) if (fieldName && (fieldName.toLowerCase().endsWith("_id") || fieldName.toLowerCase().endsWith("id"))) ret = "ID";
832
894
  else ret = "String";
833
- if (isIDLikeSQLTypeString(sqlType)) ret = "ID";
834
- if (isBooleanSQLTypeString(sqlType)) ret = "Boolean";
835
- if (isDateTimeLikeSQLTypeString(sqlType)) ret = "DateTime";
836
- if (isDateLikeSQLTypeString(sqlType)) ret = "Date";
837
- if (isJSONLikeSQLTypeString(sqlType)) ret = "JSON";
895
+ if (isIDLikeSQLTypeString(normalized)) ret = "ID";
896
+ if (isBooleanSQLTypeString(normalized)) ret = "Boolean";
897
+ if (isDateTimeLikeSQLTypeString(normalized)) ret = "DateTime";
898
+ if (isDateLikeSQLTypeString(normalized)) ret = "Date";
899
+ if (isJSONLikeSQLTypeString(normalized)) ret = "JSON";
900
+ if (isJSONFallbackSQLTypeString(normalized)) ret = "JSON";
901
+ if (isBytesSQLTypeString(normalized)) ret = "Bytes";
838
902
  if (ret !== void 0) return ret;
839
903
  throw UnknownTypeRumbleError(sqlType, "SQL to GQL");
840
904
  }
@@ -844,6 +908,60 @@ const makeDefaultName = (dbName) => `${(0, es_toolkit.capitalize)((0, drizzle_or
844
908
  const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
845
909
  const referenceStorage = /* @__PURE__ */ new Map();
846
910
  const enumArrayWhereInputStorage = /* @__PURE__ */ new Map();
911
+ const scalarArrayWhereInputStorage = /* @__PURE__ */ new Map();
912
+ const scalarArrayWhereInputName = (gqlType) => `${gqlType}ArrayWhereInputArgument`;
913
+ const getScalarArrayWhereInput = (gqlType) => {
914
+ const typeName = scalarArrayWhereInputName(gqlType);
915
+ const existing = scalarArrayWhereInputStorage.get(typeName);
916
+ if (existing) return existing;
917
+ const ret = schemaBuilder.inputRef(typeName);
918
+ scalarArrayWhereInputStorage.set(typeName, ret);
919
+ ret.implement({ fields: (t) => ({
920
+ eq: t.field({
921
+ type: [gqlType],
922
+ required: false
923
+ }),
924
+ ne: t.field({
925
+ type: [gqlType],
926
+ required: false
927
+ }),
928
+ in: t.field({
929
+ type: [[gqlType]],
930
+ required: false
931
+ }),
932
+ notIn: t.field({
933
+ type: [[gqlType]],
934
+ required: false
935
+ }),
936
+ isNull: t.boolean({ required: false }),
937
+ isNotNull: t.boolean({ required: false }),
938
+ arrayOverlaps: t.field({
939
+ type: [gqlType],
940
+ required: false
941
+ }),
942
+ arrayContained: t.field({
943
+ type: [gqlType],
944
+ required: false
945
+ }),
946
+ arrayContains: t.field({
947
+ type: [gqlType],
948
+ required: false
949
+ }),
950
+ AND: t.field({
951
+ type: [ret],
952
+ required: false
953
+ }),
954
+ OR: t.field({
955
+ type: [ret],
956
+ required: false
957
+ }),
958
+ NOT: t.field({
959
+ type: ret,
960
+ required: false
961
+ })
962
+ }) });
963
+ return ret;
964
+ };
847
965
  const getEnumArrayWhereInput = (enumImpl) => {
848
966
  const typeName = `${enumImpl.name}WhereInputArgument`;
849
967
  const existing = enumArrayWhereInputStorage.get(typeName);
@@ -907,31 +1025,73 @@ const createWhereArgImplementer = ({ db, schemaBuilder, enumImplementer }) => {
907
1025
  const ret = schemaBuilder.inputRef(inputTypeName);
908
1026
  referenceStorage.set(inputTypeName, ret);
909
1027
  ret.implement({ fields: (t) => {
910
- const mapSQLTypeStringToInputPothosType = (sqlType, fieldName) => {
1028
+ const mapSQLTypeStringToInputPothosType = (sqlType, fieldName, isArray) => {
911
1029
  const gqlType = mapSQLTypeToGraphQLType({
912
1030
  sqlType,
913
1031
  fieldName
914
1032
  });
1033
+ if (isArray) return t.field({
1034
+ type: getScalarArrayWhereInput(gqlType),
1035
+ required: false
1036
+ });
915
1037
  switch (gqlType) {
916
- case "Int": return t.field({ type: "IntWhereInputArgument" });
917
- case "String": return t.field({ type: "StringWhereInputArgument" });
918
- case "Boolean": return t.field({ type: "BooleanWhereInputArgument" });
919
- case "Date": return t.field({ type: "DateWhereInputArgument" });
920
- case "DateTime": return t.field({ type: "DateWhereInputArgument" });
921
- case "Float": return t.field({ type: "FloatWhereInputArgument" });
922
- case "ID": return t.field({ type: "IDWhereInputArgument" });
923
- case "JSON": return t.field({ type: "JSONWhereInputArgument" });
1038
+ case "Int": return t.field({
1039
+ type: "IntWhereInputArgument",
1040
+ required: false
1041
+ });
1042
+ case "BigInt": return t.field({
1043
+ type: "BigIntWhereInputArgument",
1044
+ required: false
1045
+ });
1046
+ case "String": return t.field({
1047
+ type: "StringWhereInputArgument",
1048
+ required: false
1049
+ });
1050
+ case "Boolean": return t.field({
1051
+ type: "BooleanWhereInputArgument",
1052
+ required: false
1053
+ });
1054
+ case "Date": return t.field({
1055
+ type: "DateWhereInputArgument",
1056
+ required: false
1057
+ });
1058
+ case "DateTime": return t.field({
1059
+ type: "DateTimeWhereInputArgument",
1060
+ required: false
1061
+ });
1062
+ case "Float": return t.field({
1063
+ type: "FloatWhereInputArgument",
1064
+ required: false
1065
+ });
1066
+ case "ID": return t.field({
1067
+ type: "IDWhereInputArgument",
1068
+ required: false
1069
+ });
1070
+ case "JSON": return t.field({
1071
+ type: "JSONWhereInputArgument",
1072
+ required: false
1073
+ });
1074
+ case "Bytes": return null;
924
1075
  default: throw new RumbleError(`Unsupported argument type ${gqlType} for column ${sqlType}`);
925
1076
  }
926
1077
  };
927
1078
  const fields = Object.entries(tableSchema.columns).reduce((acc, [key, value]) => {
1079
+ if (key.startsWith("__")) return acc;
1080
+ let isArray = false;
1081
+ if ((0, drizzle_orm.is)(value, drizzle_orm_pg_core.PgColumn)) {
1082
+ const dims = value.dimensions;
1083
+ if (dims && dims > 0) isArray = true;
1084
+ }
928
1085
  if (isEnumSchema(value)) {
929
1086
  const enumImpl = enumImplementer({ enumColumn: value });
930
1087
  acc[key] = t.field({
931
- type: value.dimensions > 0 ? getEnumArrayWhereInput(enumImpl) : enumImpl,
1088
+ type: isArray ? getEnumArrayWhereInput(enumImpl) : enumImpl,
932
1089
  required: false
933
1090
  });
934
- } else acc[key] = mapSQLTypeStringToInputPothosType(value.getSQLType(), key);
1091
+ } else {
1092
+ const field = mapSQLTypeStringToInputPothosType(value.getSQLType(), key, isArray);
1093
+ if (field !== null) acc[key] = field;
1094
+ }
935
1095
  return acc;
936
1096
  }, {});
937
1097
  const relations = Object.entries(tableSchema.relations ?? {}).reduce((acc, [key, value]) => {
@@ -1145,6 +1305,11 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1145
1305
  });
1146
1306
  switch (gqlType) {
1147
1307
  case "Int": return isArray ? builder.exposeIntList(fieldName, { nullable }) : builder.exposeInt(fieldName, { nullable });
1308
+ case "BigInt": return builder.field({
1309
+ type: isArray ? ["BigInt"] : "BigInt",
1310
+ resolve: (element) => element[fieldName],
1311
+ nullable
1312
+ });
1148
1313
  case "String": return isArray ? builder.exposeStringList(fieldName, { nullable }) : builder.exposeString(fieldName, { nullable });
1149
1314
  case "Boolean": return isArray ? builder.exposeBooleanList(fieldName, { nullable }) : builder.exposeBoolean(fieldName, { nullable });
1150
1315
  case "Date": return builder.field({
@@ -1154,7 +1319,11 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1154
1319
  });
1155
1320
  case "DateTime": return builder.field({
1156
1321
  type: isArray ? ["DateTime"] : "DateTime",
1157
- resolve: (element) => element[fieldName],
1322
+ resolve: (element) => {
1323
+ const v = element[fieldName];
1324
+ if (v == null) return v;
1325
+ return v instanceof Date ? v : new Date(v);
1326
+ },
1158
1327
  nullable
1159
1328
  });
1160
1329
  case "Float": return isArray ? builder.exposeFloatList(fieldName, { nullable }) : builder.exposeFloat(fieldName, { nullable });
@@ -1164,6 +1333,15 @@ function buildPothosResponseTypeFromGraphQLType({ builder, sqlType, fieldName, n
1164
1333
  resolve: (element) => element[fieldName],
1165
1334
  nullable
1166
1335
  });
1336
+ case "Bytes": return builder.field({
1337
+ type: "Bytes",
1338
+ resolve: (element) => {
1339
+ const v = element[fieldName];
1340
+ if (v == null) return v;
1341
+ return Buffer.isBuffer(v) ? v.toString("base64") : String(v);
1342
+ },
1343
+ nullable
1344
+ });
1167
1345
  default: throw new RumbleError(`Unsupported object type ${gqlType} for column ${fieldName}`);
1168
1346
  }
1169
1347
  }
@@ -1195,14 +1373,17 @@ function isPostgresDB(db) {
1195
1373
  //#endregion
1196
1374
  //#region lib/search.ts
1197
1375
  async function initSearchIfApplicable(input) {
1376
+ const log = input.logger?.enabled ? input.logger.logger : void 0;
1198
1377
  if (!isPostgresDB(input.db)) {
1199
- console.info("Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.");
1378
+ const msg = "Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.";
1379
+ log ? log.info({}, msg) : console.info(msg);
1200
1380
  return;
1201
1381
  }
1202
1382
  try {
1203
1383
  await input.db.execute(drizzle_orm.sql`CREATE EXTENSION IF NOT EXISTS pg_trgm;`);
1204
1384
  } catch (error) {
1205
- 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);
1385
+ 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.";
1386
+ log ? log.error({ err: error }, msg) : console.error(msg, error);
1206
1387
  return;
1207
1388
  }
1208
1389
  if (input.search?.threshold) {
@@ -1261,13 +1442,17 @@ const isProbablyAConfigObject = (t) => {
1261
1442
  ].find((e) => e === k))) return true;
1262
1443
  return false;
1263
1444
  };
1264
- const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1445
+ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1265
1446
  return ({ table, refName, readAction = "read", adjust }) => {
1266
1447
  const tableSchema = tableHelper({
1267
1448
  db,
1268
1449
  table
1269
1450
  });
1270
- if (Object.keys(tableSchema.primaryKey).length === 0) console.warn(`Could not find primary key for ${table.toString()}. Cannot register subscriptions!`);
1451
+ if (Object.keys(tableSchema.primaryKey).length === 0) {
1452
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1453
+ const msg = `Could not find primary key for ${table.toString()}. Cannot register subscriptions!`;
1454
+ log ? log.warn({ "rumble.table": table.toString() }, msg) : console.warn(msg);
1455
+ }
1271
1456
  const primaryKey = Object.values(tableSchema.primaryKey)[0];
1272
1457
  const { registerOnInstance } = makePubSubInstance({ table });
1273
1458
  return schemaBuilder.drizzleObject(table, {
@@ -1276,7 +1461,9 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
1276
1461
  if (!primaryKey) return;
1277
1462
  const primaryKeyValue = element[primaryKey.name];
1278
1463
  if (!primaryKeyValue) {
1279
- console.warn(`Could not find primary key value for ${JSON.stringify(element)}. Cannot register subscription!`);
1464
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1465
+ const msg = `Could not find primary key value for element on ${table.toString()}. Cannot register subscription!`;
1466
+ log ? log.warn({ "rumble.table": table.toString() }, msg) : console.warn(msg);
1280
1467
  return;
1281
1468
  }
1282
1469
  registerOnInstance({
@@ -1307,6 +1494,10 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
1307
1494
  };
1308
1495
  } })) ?? {};
1309
1496
  const fields = Object.entries(columns).reduce((acc, [key, column]) => {
1497
+ if (key.startsWith("__")) {
1498
+ 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.`);
1499
+ return acc;
1500
+ }
1310
1501
  if (userAdjustments[key]) {
1311
1502
  const { params, creatorFunction, configObject } = configMap.get(userAdjustments[key]);
1312
1503
  if (typeof configObject.nullable !== "boolean") configObject.nullable = !column.notNull;
@@ -1507,7 +1698,8 @@ const createPubSubInstance = ({ subscriptions }) => {
1507
1698
  };
1508
1699
  //#endregion
1509
1700
  //#region lib/query.ts
1510
- const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1701
+ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1702
+ const registeredQueryFieldNames = /* @__PURE__ */ new Set();
1511
1703
  return ({ table, readAction = "read", listAction = "read" }) => {
1512
1704
  const WhereArg = whereArgImplementer({ table });
1513
1705
  const OrderArg = orderArgImplementer({ table });
@@ -1517,6 +1709,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1517
1709
  });
1518
1710
  const primaryKeyField = Object.values(tableSchema.primaryKey)[0];
1519
1711
  const { registerOnInstance } = makePubSubInstance({ table });
1712
+ const pluralName = pluralize.default.plural(table.toString());
1713
+ const singularName = pluralize.default.singular(table.toString());
1714
+ for (const name of [pluralName, singularName]) {
1715
+ 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").`);
1716
+ registeredQueryFieldNames.add(name);
1717
+ }
1520
1718
  return schemaBuilder.queryFields((t) => {
1521
1719
  const manyArgs = {
1522
1720
  where: t.arg({
@@ -1533,11 +1731,11 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1533
1731
  };
1534
1732
  if (!search?.enabled) delete manyArgs.search;
1535
1733
  return {
1536
- [pluralize.default.plural(table.toString())]: t.drizzleField({
1734
+ [pluralName]: t.drizzleField({
1537
1735
  type: [table],
1538
1736
  nullable: false,
1539
1737
  smartSubscription: true,
1540
- description: `List all ${pluralize.default.plural(table.toString())}`,
1738
+ description: `List all ${pluralName}`,
1541
1739
  subscribe: (subscriptions, _root, _args, _ctx, _info) => {
1542
1740
  registerOnInstance({
1543
1741
  instance: subscriptions,
@@ -1565,17 +1763,21 @@ const createQueryImplementer = ({ db, schemaBuilder, search, whereArgImplementer
1565
1763
  if (filter.columns) queryInstance.columns = filter.columns;
1566
1764
  if (search?.enabled && args.search && args.search.length > 0 && search?.cpu_operator_cost) return db.transaction(async (tx) => {
1567
1765
  if (isPostgresDB(tx)) await tx.execute(drizzle_orm.sql.raw(`SET cpu_operator_cost = ${search.cpu_operator_cost};`));
1568
- else console.info("Database dialect is not postgresql, cannot set cpu_operator_cost.");
1766
+ else {
1767
+ const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
1768
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1769
+ log ? log.info({}, msg) : console.info(msg);
1770
+ }
1569
1771
  return tx.query[table].findMany(queryInstance);
1570
1772
  });
1571
1773
  return db.query[table].findMany(queryInstance);
1572
1774
  }
1573
1775
  }),
1574
- [pluralize.default.singular(table.toString())]: t.drizzleField({
1776
+ [singularName]: t.drizzleField({
1575
1777
  type: table,
1576
1778
  nullable: false,
1577
1779
  smartSubscription: true,
1578
- description: `Get a single ${pluralize.default.singular(table.toString())} by ID`,
1780
+ description: `Get a single ${singularName} by ID`,
1579
1781
  args: { id: t.arg.id({ required: true }) },
1580
1782
  resolve: (query, _root, args, ctx, _info) => {
1581
1783
  deepSetProto(args);
@@ -1751,6 +1953,96 @@ function implementDefaultWhereInputArgs(schemaBuilder) {
1751
1953
  OR: t.field({ type: [JSONWhereInputArgument] }),
1752
1954
  NOT: t.field({ type: JSONWhereInputArgument })
1753
1955
  }) });
1956
+ const BigIntWhereInputArgument = schemaBuilder.inputRef("BigIntWhereInputArgument").implement({ fields: (t) => ({
1957
+ eq: t.field({
1958
+ type: "BigInt",
1959
+ required: false
1960
+ }),
1961
+ ne: t.field({
1962
+ type: "BigInt",
1963
+ required: false
1964
+ }),
1965
+ gt: t.field({
1966
+ type: "BigInt",
1967
+ required: false
1968
+ }),
1969
+ gte: t.field({
1970
+ type: "BigInt",
1971
+ required: false
1972
+ }),
1973
+ lt: t.field({
1974
+ type: "BigInt",
1975
+ required: false
1976
+ }),
1977
+ lte: t.field({
1978
+ type: "BigInt",
1979
+ required: false
1980
+ }),
1981
+ in: t.field({
1982
+ type: ["BigInt"],
1983
+ required: false
1984
+ }),
1985
+ notIn: t.field({
1986
+ type: ["BigInt"],
1987
+ required: false
1988
+ }),
1989
+ isNull: t.boolean(),
1990
+ isNotNull: t.boolean(),
1991
+ AND: t.field({ type: [BigIntWhereInputArgument] }),
1992
+ OR: t.field({ type: [BigIntWhereInputArgument] }),
1993
+ NOT: t.field({ type: BigIntWhereInputArgument })
1994
+ }) });
1995
+ const DateTimeWhereInputArgument = schemaBuilder.inputRef("DateTimeWhereInputArgument").implement({ fields: (t) => ({
1996
+ eq: t.field({
1997
+ type: "DateTime",
1998
+ required: false
1999
+ }),
2000
+ ne: t.field({
2001
+ type: "DateTime",
2002
+ required: false
2003
+ }),
2004
+ gt: t.field({
2005
+ type: "DateTime",
2006
+ required: false
2007
+ }),
2008
+ gte: t.field({
2009
+ type: "DateTime",
2010
+ required: false
2011
+ }),
2012
+ lt: t.field({
2013
+ type: "DateTime",
2014
+ required: false
2015
+ }),
2016
+ lte: t.field({
2017
+ type: "DateTime",
2018
+ required: false
2019
+ }),
2020
+ in: t.field({
2021
+ type: ["DateTime"],
2022
+ required: false
2023
+ }),
2024
+ notIn: t.field({
2025
+ type: ["DateTime"],
2026
+ required: false
2027
+ }),
2028
+ isNull: t.boolean(),
2029
+ isNotNull: t.boolean(),
2030
+ arrayOverlaps: t.field({
2031
+ type: ["DateTime"],
2032
+ required: false
2033
+ }),
2034
+ arrayContained: t.field({
2035
+ type: ["DateTime"],
2036
+ required: false
2037
+ }),
2038
+ arrayContains: t.field({
2039
+ type: ["DateTime"],
2040
+ required: false
2041
+ }),
2042
+ AND: t.field({ type: [DateTimeWhereInputArgument] }),
2043
+ OR: t.field({ type: [DateTimeWhereInputArgument] }),
2044
+ NOT: t.field({ type: DateTimeWhereInputArgument })
2045
+ }) });
1754
2046
  }
1755
2047
  //#endregion
1756
2048
  //#region lib/runtimeFiltersPlugin/filterTypes.ts
@@ -1761,9 +2053,11 @@ const applyFiltersKey = "applyFilters";
1761
2053
  var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
1762
2054
  tracer;
1763
2055
  tracerEnabled;
2056
+ logger;
1764
2057
  onTypeConfig(typeConfig) {
1765
2058
  this.tracer = this.builder.options.otel?.tracer;
1766
2059
  this.tracerEnabled = this.builder.options.otel?.enabled;
2060
+ this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
1767
2061
  return typeConfig;
1768
2062
  }
1769
2063
  wrapResolve(resolver, fieldConfig) {
@@ -1792,7 +2086,8 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
1792
2086
  }));
1793
2087
  let resolved, prefetchedFilters;
1794
2088
  if (this.tracer && this.tracerEnabled) {
1795
- const o = await this.tracer.startActiveSpan(`apply_filters`, async (span) => {
2089
+ const o = await this.tracer.startActiveSpan(`rumble.filter.resolve`, async (span) => {
2090
+ span.setAttribute("graphql.field.name", fieldConfig.name);
1796
2091
  try {
1797
2092
  return await Promise.all([resolver(parent, args, context, info), prefetchedFiltersPromises]);
1798
2093
  } finally {
@@ -1814,17 +2109,37 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
1814
2109
  return acc;
1815
2110
  }, /* @__PURE__ */ new Set()));
1816
2111
  span?.setAttribute("filters.allowed", allowed.length);
2112
+ this.logger?.debug({
2113
+ "graphql.field.name": fieldConfig.name,
2114
+ "filters.total": allFilters.length,
2115
+ "filters.allowed": allowed.length
2116
+ }, "runtime filters applied");
1817
2117
  if (Array.isArray(resolved)) return allowed;
1818
2118
  return allowed[0] ?? null;
1819
2119
  };
1820
- if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`filter_${fieldConfig.name}`, async (span) => {
2120
+ if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`rumble.filter.apply`, async (span) => {
2121
+ span.setAttribute("graphql.field.name", fieldConfig.name);
1821
2122
  try {
1822
2123
  return await runFilters(span);
2124
+ } catch (error) {
2125
+ this.logger?.error({
2126
+ "graphql.field.name": fieldConfig.name,
2127
+ err: error
2128
+ }, "runtime filter threw");
2129
+ throw error;
1823
2130
  } finally {
1824
2131
  span.end();
1825
2132
  }
1826
2133
  });
1827
- else return runFilters();
2134
+ else try {
2135
+ return await runFilters();
2136
+ } catch (error) {
2137
+ this.logger?.error({
2138
+ "graphql.field.name": fieldConfig.name,
2139
+ err: error
2140
+ }, "runtime filter threw");
2141
+ throw error;
2142
+ }
1828
2143
  };
1829
2144
  }
1830
2145
  };
@@ -1837,7 +2152,7 @@ function registerRuntimeFiltersPlugin() {
1837
2152
  }
1838
2153
  //#endregion
1839
2154
  //#region lib/schemaBuilder.ts
1840
- const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig, otel }) => {
2155
+ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
1841
2156
  const createSpan = otel?.enabled && otel.tracer ? (0, _pothos_tracing_opentelemetry.createOpenTelemetryWrapper)(otel.tracer, otel.options) : void 0;
1842
2157
  registerRuntimeFiltersPlugin();
1843
2158
  const schemaBuilder = new _pothos_core.default({
@@ -1865,18 +2180,40 @@ const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig,
1865
2180
  }) },
1866
2181
  defaultFieldNullability: false,
1867
2182
  tracing: {
1868
- default: otel?.enabled ? (config) => (0, _pothos_plugin_tracing.isRootField)(config) : () => false,
1869
- wrap: createSpan ? (resolver, options) => createSpan(resolver, options) : (resolver) => resolver
2183
+ default: otel?.enabled || logger?.enabled ? (config) => (0, _pothos_plugin_tracing.isRootField)(config) : () => false,
2184
+ wrap: (resolver, options, config) => {
2185
+ let r = createSpan ? createSpan(resolver, options) : resolver;
2186
+ if (logger?.enabled) {
2187
+ const log = logger.logger;
2188
+ r = (0, _pothos_plugin_tracing.wrapResolver)(r, (error, duration) => {
2189
+ if (error) log.error({
2190
+ "graphql.field.name": config.name,
2191
+ "graphql.parent.type": config.parentType,
2192
+ durationMs: duration,
2193
+ err: error
2194
+ }, "resolver failed");
2195
+ else log.debug({
2196
+ "graphql.field.name": config.name,
2197
+ "graphql.parent.type": config.parentType,
2198
+ durationMs: duration
2199
+ }, "resolver completed");
2200
+ });
2201
+ }
2202
+ return r;
2203
+ }
1870
2204
  },
1871
- otel
2205
+ otel,
2206
+ logger
1872
2207
  });
1873
2208
  schemaBuilder.addScalarType("JSON", graphql_scalars.JSONResolver);
1874
2209
  schemaBuilder.addScalarType("Date", graphql_scalars.DateResolver);
1875
2210
  schemaBuilder.addScalarType("DateTime", graphql_scalars.DateTimeISOResolver);
2211
+ schemaBuilder.addScalarType("BigInt", graphql_scalars.BigIntResolver);
2212
+ schemaBuilder.addScalarType("Bytes", graphql_scalars.ByteResolver);
1876
2213
  implementDefaultWhereInputArgs(schemaBuilder);
1877
- if (!disableDefaultObjects?.query) schemaBuilder.queryType({});
1878
- if (!disableDefaultObjects?.subscription) schemaBuilder.subscriptionType({});
1879
- if (!disableDefaultObjects?.mutation) schemaBuilder.mutationType({});
2214
+ schemaBuilder.queryType({});
2215
+ schemaBuilder.subscriptionType({});
2216
+ schemaBuilder.mutationType({});
1880
2217
  return { schemaBuilder };
1881
2218
  };
1882
2219
  //#endregion
@@ -1959,7 +2296,33 @@ export const r = rumble({
1959
2296
  whereArgImplementer: whereArg,
1960
2297
  makePubSubInstance
1961
2298
  });
1962
- const builtSchema = require_lazy.lazy(() => schemaBuilder.toSchema());
2299
+ const builtSchema = require_lazy.lazy(() => {
2300
+ let raw;
2301
+ try {
2302
+ raw = schemaBuilder.toSchema();
2303
+ } catch (e) {
2304
+ const unimplMatch = (e?.message ?? String(e)).match(/ObjectRef<(.+?)> has not been implemented/);
2305
+ 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.`);
2306
+ throw e;
2307
+ }
2308
+ const mutation = raw.getMutationType();
2309
+ const subscription = raw.getSubscriptionType();
2310
+ const emptyMutation = mutation && Object.keys(mutation.getFields()).length === 0;
2311
+ const emptySubscription = subscription && Object.keys(subscription.getFields()).length === 0;
2312
+ if (!emptyMutation && !emptySubscription) return raw;
2313
+ const config = raw.toConfig();
2314
+ const filteredTypes = config.types.filter((t) => {
2315
+ if (emptyMutation && t === mutation) return false;
2316
+ if (emptySubscription && t === subscription) return false;
2317
+ return true;
2318
+ });
2319
+ return new graphql.GraphQLSchema({
2320
+ ...config,
2321
+ mutation: emptyMutation ? void 0 : config.mutation,
2322
+ subscription: emptySubscription ? void 0 : config.subscription,
2323
+ types: filteredTypes
2324
+ });
2325
+ });
1963
2326
  const createYoga = (args) => {
1964
2327
  const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
1965
2328
  return (0, graphql_yoga.createYoga)({
@@ -1970,26 +2333,60 @@ export const r = rumble({
1970
2333
  plugins: [
1971
2334
  ...args?.plugins ?? [],
1972
2335
  ...enableApiDocs ? [] : [(0, _graphql_yoga_plugin_disable_introspection.useDisableIntrospection)(), (0, _escape_tech_graphql_armor.EnvelopArmorPlugin)(args?.armorConfig)],
1973
- rumbleInput.otel?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
1974
- setExecuteFn((options) => rumbleInput.otel.tracer.startActiveSpan(_pothos_tracing_opentelemetry.SpanNames.EXECUTE, { attributes: {
1975
- [_pothos_tracing_opentelemetry.AttributeNames.OPERATION_NAME]: options.operationName ?? "anonymous",
1976
- [_pothos_tracing_opentelemetry.AttributeNames.SOURCE]: options.document
1977
- } }, async (span) => {
1978
- try {
2336
+ rumbleInput.otel?.enabled || rumbleInput.logger?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
2337
+ setExecuteFn(async (options) => {
2338
+ const log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
2339
+ const operationName = options.operationName ?? "anonymous";
2340
+ const start = Date.now();
2341
+ log?.info({ "graphql.operation.name": operationName }, "graphql execute start");
2342
+ const run = async () => {
1979
2343
  const result = await executeFn(options);
1980
- if (result && "errors" in result && result.errors?.length) {
1981
- for (const error of result.errors) span.recordException(error);
2344
+ if (result && "errors" in result && result.errors?.length) log?.error({
2345
+ "graphql.operation.name": operationName,
2346
+ durationMs: Date.now() - start,
2347
+ errors: result.errors.map((e) => e.message)
2348
+ }, "graphql execute completed with errors");
2349
+ else log?.info({
2350
+ "graphql.operation.name": operationName,
2351
+ durationMs: Date.now() - start
2352
+ }, "graphql execute completed");
2353
+ return result;
2354
+ };
2355
+ if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(_pothos_tracing_opentelemetry.SpanNames.EXECUTE, { attributes: {
2356
+ [_pothos_tracing_opentelemetry.AttributeNames.OPERATION_NAME]: operationName,
2357
+ [_pothos_tracing_opentelemetry.AttributeNames.SOURCE]: options.document
2358
+ } }, async (span) => {
2359
+ try {
2360
+ const result = await run();
2361
+ if (result && "errors" in result && result.errors?.length) {
2362
+ for (const error of result.errors) span.recordException(error);
2363
+ span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2364
+ }
2365
+ return result;
2366
+ } catch (error) {
2367
+ if (error instanceof Error) span.recordException(error);
2368
+ log?.error({
2369
+ "graphql.operation.name": operationName,
2370
+ durationMs: Date.now() - start,
2371
+ err: error
2372
+ }, "graphql execute threw");
1982
2373
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2374
+ throw error;
2375
+ } finally {
2376
+ span.end();
1983
2377
  }
1984
- return result;
2378
+ });
2379
+ try {
2380
+ return await run();
1985
2381
  } catch (error) {
1986
- if (error instanceof Error) span.recordException(error);
1987
- span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2382
+ log?.error({
2383
+ "graphql.operation.name": operationName,
2384
+ durationMs: Date.now() - start,
2385
+ err: error
2386
+ }, "graphql execute threw");
1988
2387
  throw error;
1989
- } finally {
1990
- span.end();
1991
2388
  }
1992
- }));
2389
+ });
1993
2390
  } } : false
1994
2391
  ].filter(Boolean)
1995
2392
  });