@vantreeseba/drizzle-graphql 5.0.0 → 5.2.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/dist/index.d.cts CHANGED
@@ -57,12 +57,24 @@ type FilterColumnOperatorsCore<TColumn extends Column, TColType = GetColumnGqlDa
57
57
  notIlike: string;
58
58
  startsWith: string;
59
59
  endsWith: string;
60
- contains: string;
60
+ /**
61
+ * String columns: safe substring match (`%`, `_` and `\` matched literally).
62
+ * JSON columns: structural containment (Postgres `@>` / MySQL JSON_CONTAINS).
63
+ */
64
+ contains: string | TColType;
61
65
  iStartsWith: string;
62
66
  iEndsWith: string;
63
67
  iContains: string;
64
68
  inArray: Array<TColType>;
65
69
  notInArray: Array<TColType>;
70
+ /** Array columns only: the array contains this element. */
71
+ has: TColType extends Array<infer TElement> ? TElement : never;
72
+ /** Array columns only: the array overlaps these elements (`&&`). */
73
+ hasSome: TColType extends Array<infer TElement> ? Array<TElement> : never;
74
+ /** Array columns only: the array contains all of these elements (`@>`). */
75
+ hasEvery: TColType extends Array<infer TElement> ? Array<TElement> : never;
76
+ /** Array columns only: when true, matches arrays with no elements. */
77
+ isEmpty: boolean;
66
78
  isNull: boolean;
67
79
  isNotNull: boolean;
68
80
  }>;
package/dist/index.d.ts CHANGED
@@ -57,12 +57,24 @@ type FilterColumnOperatorsCore<TColumn extends Column, TColType = GetColumnGqlDa
57
57
  notIlike: string;
58
58
  startsWith: string;
59
59
  endsWith: string;
60
- contains: string;
60
+ /**
61
+ * String columns: safe substring match (`%`, `_` and `\` matched literally).
62
+ * JSON columns: structural containment (Postgres `@>` / MySQL JSON_CONTAINS).
63
+ */
64
+ contains: string | TColType;
61
65
  iStartsWith: string;
62
66
  iEndsWith: string;
63
67
  iContains: string;
64
68
  inArray: Array<TColType>;
65
69
  notInArray: Array<TColType>;
70
+ /** Array columns only: the array contains this element. */
71
+ has: TColType extends Array<infer TElement> ? TElement : never;
72
+ /** Array columns only: the array overlaps these elements (`&&`). */
73
+ hasSome: TColType extends Array<infer TElement> ? Array<TElement> : never;
74
+ /** Array columns only: the array contains all of these elements (`@>`). */
75
+ hasEvery: TColType extends Array<infer TElement> ? Array<TElement> : never;
76
+ /** Array columns only: when true, matches arrays with no elements. */
77
+ isEmpty: boolean;
66
78
  isNull: boolean;
67
79
  isNotNull: boolean;
68
80
  }>;
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  import {
13
13
  aliasedTable,
14
14
  and,
15
+ arrayContains,
16
+ arrayOverlaps,
15
17
  asc,
16
18
  desc,
17
19
  eq,
@@ -416,6 +418,21 @@ var geoXyInputType = new GraphQLInputObjectType({
416
418
  }
417
419
  });
418
420
  var columnToGraphQLCore = (column, columnName, tableName, isInput) => {
421
+ const dimensions = column.dimensions;
422
+ if (dimensions !== void 0 && dimensions > 0) {
423
+ const inner = columnBaseToGraphQLCore(column, columnName, tableName, isInput);
424
+ const innerDesc = inner.description ?? inner.type.name;
425
+ let type = inner.type;
426
+ let description = innerDesc;
427
+ for (let i = 0; i < dimensions; i++) {
428
+ type = new GraphQLList(new GraphQLNonNull(type));
429
+ description = `Array<${description}>`;
430
+ }
431
+ return { type, description };
432
+ }
433
+ return columnBaseToGraphQLCore(column, columnName, tableName, isInput);
434
+ };
435
+ var columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
419
436
  const { type: baseType, constraint } = extractExtendedColumnType(column);
420
437
  switch (baseType) {
421
438
  case "boolean":
@@ -447,28 +464,10 @@ var columnToGraphQLCore = (column, columnName, tableName, isInput) => {
447
464
  if (column instanceof PgDateString) {
448
465
  return isInput ? { type: GraphQLString, description: "Date" } : { type: GraphQLDate, description: "Date" };
449
466
  }
450
- {
451
- const dims = column.dimensions;
452
- if (dims !== void 0 && dims > 0) {
453
- return {
454
- type: new GraphQLList(new GraphQLNonNull(GraphQLString)),
455
- description: "Array<String>"
456
- };
457
- }
458
- }
459
467
  return { type: GraphQLString, description: "String" };
460
468
  case "bigint":
461
469
  return { type: GraphQLBigIntString, description: "BigInt" };
462
470
  case "number": {
463
- const dims = column.dimensions;
464
- if (dims !== void 0 && dims > 0) {
465
- const baseDesc = is2(column, PgInteger) || is2(column, PgSerial) ? "Integer" : "Float";
466
- const baseType2 = baseDesc === "Integer" ? GraphQLInt : GraphQLFloat;
467
- return {
468
- type: new GraphQLList(new GraphQLNonNull(baseType2)),
469
- description: `Array<${baseDesc}>`
470
- };
471
- }
472
471
  return is2(column, PgInteger) || is2(column, PgSerial) || is2(column, MySqlInt) || is2(column, MySqlSerial) || is2(column, SQLiteInteger) ? { type: GraphQLInt, description: "Integer" } : { type: GraphQLFloat, description: "Float" };
473
472
  }
474
473
  case "array": {
@@ -813,45 +812,95 @@ var innerOrder = new GraphQLInputObjectType2({
813
812
  }
814
813
  }
815
814
  });
816
- var resolveGenericFilterName = (column, columnName, columnGraphQLType) => {
817
- if (columnName === "id" || columnName.endsWith("Id")) {
818
- return "Id";
815
+ var columnDialect = (column) => {
816
+ const ct = column.columnType ?? "";
817
+ if (ct.startsWith("Pg")) {
818
+ return "pg";
819
+ }
820
+ if (ct.startsWith("MySql")) {
821
+ return "mysql";
822
+ }
823
+ if (ct.startsWith("SQLite")) {
824
+ return "sqlite";
825
+ }
826
+ return void 0;
827
+ };
828
+ var resolveGenericFilterDescriptor = (column, columnGraphQLType) => {
829
+ if (columnGraphQLType.type === GraphQLJSON) {
830
+ return { name: "JSON", kind: "json" };
831
+ }
832
+ if (columnGraphQLType.type instanceof GraphQLList2) {
833
+ let element = columnGraphQLType.type.ofType;
834
+ if (element instanceof GraphQLNonNull2) {
835
+ element = element.ofType;
836
+ }
837
+ return { name: `${element.name}Array`, kind: "array" };
838
+ }
839
+ if (columnGraphQLType.type === GraphQLUUID) {
840
+ return { name: "Id", kind: "scalar" };
819
841
  }
820
842
  if (columnGraphQLType.type === GraphQLBoolean2) {
821
- return "Boolean";
843
+ return { name: "Boolean", kind: "scalar" };
822
844
  }
823
845
  if (columnGraphQLType.type instanceof GraphQLEnumType2) {
824
- return columnGraphQLType.type.name;
846
+ return { name: columnGraphQLType.type.name, kind: "scalar" };
825
847
  }
826
848
  if (columnGraphQLType.type === GraphQLBigIntString) {
827
- return "BigInt";
849
+ return { name: "BigInt", kind: "scalar" };
828
850
  }
829
851
  if (columnGraphQLType.type === GraphQLDecimalString) {
830
- return "Decimal";
831
- }
832
- if (columnGraphQLType.type instanceof GraphQLList2) {
833
- const desc2 = columnGraphQLType.description ?? "";
834
- let inner = columnGraphQLType.type.ofType;
835
- if (inner instanceof GraphQLNonNull2) {
836
- inner = inner.ofType;
837
- }
838
- if (desc2.includes("Integer") || inner === GraphQLInt2) {
839
- return "IntArray";
840
- }
841
- if (desc2.includes("Float") || inner === GraphQLFloat2) {
842
- return "FloatArray";
843
- }
844
- return "StringArray";
852
+ return { name: "Decimal", kind: "scalar" };
845
853
  }
846
854
  const ct = column.columnType ?? "";
847
855
  if (ct === "PgTimestamp" || ct === "PgTimestampString" || ct === "PgDate") {
848
- return "DateTime";
856
+ return { name: "DateTime", kind: "scalar" };
849
857
  }
850
- return "String";
858
+ if (columnGraphQLType.type === GraphQLInt2) {
859
+ return { name: "Int", kind: "scalar" };
860
+ }
861
+ if (columnGraphQLType.type === GraphQLFloat2) {
862
+ return { name: "Float", kind: "scalar" };
863
+ }
864
+ return { name: "String", kind: "scalar" };
851
865
  };
866
+ var jsonFilterFields = (column, colType) => {
867
+ const dialect = columnDialect(column);
868
+ return {
869
+ eq: { type: colType, description: "JSON equality on the whole value" },
870
+ ne: { type: colType, description: "JSON inequality on the whole value" },
871
+ ...dialect === "pg" || dialect === "mysql" ? {
872
+ contains: {
873
+ type: colType,
874
+ description: "Value structurally contains this JSON (Postgres `@>` / MySQL JSON_CONTAINS)"
875
+ }
876
+ } : {},
877
+ isNull: { type: GraphQLBoolean2 },
878
+ isNotNull: { type: GraphQLBoolean2 }
879
+ };
880
+ };
881
+ var arrayFilterFields = (colType, colArr, colDesc) => {
882
+ let element = colType.ofType;
883
+ if (element instanceof GraphQLNonNull2) {
884
+ element = element.ofType;
885
+ }
886
+ const elementList = new GraphQLList2(new GraphQLNonNull2(element));
887
+ return {
888
+ eq: { type: colType, description: colDesc },
889
+ ne: { type: colType, description: colDesc },
890
+ has: { type: element, description: "Array contains this element" },
891
+ hasSome: { type: elementList, description: "Array contains at least one of these elements (overlap, `&&`)" },
892
+ hasEvery: { type: elementList, description: "Array contains every one of these elements (containment, `@>`)" },
893
+ isEmpty: { type: GraphQLBoolean2, description: "When true, matches arrays with no elements" },
894
+ inArray: { type: colArr, description: `Array<${colDesc}>` },
895
+ notInArray: { type: colArr, description: `Array<${colDesc}>` },
896
+ isNull: { type: GraphQLBoolean2 },
897
+ isNotNull: { type: GraphQLBoolean2 }
898
+ };
899
+ };
900
+ var FILTERS_WITHOUT_STRING_OPS = /* @__PURE__ */ new Set(["Id", "Int", "Float", "BigInt", "Decimal"]);
852
901
  var generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
853
902
  const columnGraphQLType = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
854
- const genericName = resolveGenericFilterName(column, columnName, columnGraphQLType);
903
+ const { name: genericName, kind } = resolveGenericFilterDescriptor(column, columnGraphQLType);
855
904
  const cached = cacheCtx.genericFilterCache.get(genericName);
856
905
  if (cached) {
857
906
  return cached;
@@ -859,8 +908,8 @@ var generateColumnFilterValues = (column, tableName, columnName, cacheCtx) => {
859
908
  const colType = columnGraphQLType.type;
860
909
  const colDesc = columnGraphQLType.description;
861
910
  const colArr = new GraphQLList2(new GraphQLNonNull2(colType));
862
- const omitStringOps = genericName === "Id" || genericName === "BigInt" || genericName === "Decimal";
863
- const baseFields = {
911
+ const omitStringOps = FILTERS_WITHOUT_STRING_OPS.has(genericName);
912
+ const baseFields = kind === "json" ? jsonFilterFields(column, colType) : kind === "array" ? arrayFilterFields(colType, colArr, colDesc) : {
864
913
  eq: { type: colType, description: colDesc },
865
914
  ne: { type: colType, description: colDesc },
866
915
  lt: { type: colType, description: colDesc },
@@ -1433,12 +1482,28 @@ var safeLikeCondition = (column, pattern, insensitive) => {
1433
1482
  const isPg = (column.columnType ?? "").startsWith("Pg");
1434
1483
  return isPg ? sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
1435
1484
  };
1485
+ var isJsonColumn2 = (column) => (column.columnType ?? "").includes("Json");
1486
+ var jsonContains = (column, columnName, value) => {
1487
+ const serialized = JSON.stringify(value);
1488
+ switch (columnDialect(column)) {
1489
+ case "pg":
1490
+ return column.columnType === "PgJson" ? sql`${column}::jsonb @> ${serialized}::jsonb` : sql`${column} @> ${serialized}::jsonb`;
1491
+ case "mysql":
1492
+ return sql`json_contains(${column}, ${serialized})`;
1493
+ default:
1494
+ throw new GraphQLError3(`WHERE ${columnName}: Operator 'contains' is not supported for this dialect!`);
1495
+ }
1496
+ };
1436
1497
  var extractFiltersColumn = (column, columnName, operators) => {
1437
1498
  const { OR, AND, NOT, ...restOperators } = operators;
1438
1499
  const entries = Object.entries(restOperators);
1439
1500
  const singleValueOps = { eq, ne, gt, gte, lt, lte };
1440
1501
  const stringValueOps = { like, notLike, ilike, notIlike };
1441
1502
  const arrayValueOps = { inArray, notInArray };
1503
+ const arrayMembershipOps = {
1504
+ hasSome: arrayOverlaps,
1505
+ hasEvery: arrayContains
1506
+ };
1442
1507
  const nullableOps = { isNull, isNotNull };
1443
1508
  const variants = [];
1444
1509
  for (const [operatorName, operatorValue] of entries) {
@@ -1450,6 +1515,8 @@ var extractFiltersColumn = (column, columnName, operators) => {
1450
1515
  variants.push(singleValueOps[operatorName](column, singleValue));
1451
1516
  } else if (operatorName in stringValueOps) {
1452
1517
  variants.push(stringValueOps[operatorName](column, operatorValue));
1518
+ } else if (operatorName === "contains" && isJsonColumn2(column)) {
1519
+ variants.push(jsonContains(column, columnName, operatorValue));
1453
1520
  } else if (operatorName in safeLikeOps) {
1454
1521
  const { buildPattern, insensitive } = safeLikeOps[operatorName];
1455
1522
  variants.push(safeLikeCondition(column, buildPattern(operatorValue), insensitive));
@@ -1459,6 +1526,15 @@ var extractFiltersColumn = (column, columnName, operators) => {
1459
1526
  }
1460
1527
  const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
1461
1528
  variants.push(arrayValueOps[operatorName](column, arrayValue));
1529
+ } else if (operatorName === "has") {
1530
+ variants.push(arrayContains(column, [operatorValue]));
1531
+ } else if (operatorName in arrayMembershipOps) {
1532
+ if (!operatorValue.length) {
1533
+ throw new GraphQLError3(`WHERE ${columnName}: Unable to use operator ${operatorName} with an empty array!`);
1534
+ }
1535
+ variants.push(arrayMembershipOps[operatorName](column, operatorValue));
1536
+ } else if (operatorName === "isEmpty") {
1537
+ variants.push(sql`cardinality(${column}) = 0`);
1462
1538
  } else if (operatorName in nullableOps) {
1463
1539
  variants.push(nullableOps[operatorName](column));
1464
1540
  } else {