@vantreeseba/drizzle-graphql 9.1.1 → 10.0.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/README.md +13 -0
- package/dist/index.cjs +164 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -2
- package/dist/index.d.ts +50 -2
- package/dist/index.js +155 -7
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -1831,6 +1831,19 @@ image, a stored value the scalar transporting it cannot represent, is
|
|
|
1831
1831
|
`DRIZZLE_UNREPRESENTABLE_VALUE`; it says the data is out of range, not that the request was
|
|
1832
1832
|
wrong.
|
|
1833
1833
|
|
|
1834
|
+
An `update`'s `set` giving a `NOT NULL` column an explicit `null` is `DRIZZLE_NOT_NULL`, and the
|
|
1835
|
+
error names the column. Every field of an update input is nullable — that is what makes a partial
|
|
1836
|
+
`set` possible — so the nullability says nothing about the column, and an update has no default to
|
|
1837
|
+
fall back on: `null` there can only mean "write null". Nothing is written, including the rest of
|
|
1838
|
+
the `set`. To leave a column alone, omit it.
|
|
1839
|
+
|
|
1840
|
+
An **insert** reads the same `null` the other way, and is unchanged. A `NOT NULL` column's field
|
|
1841
|
+
on a create input is non-null unless the database, drizzle, or a nested `create` can fill it in,
|
|
1842
|
+
so the only nulls that reach a resolver are ones the schema offered as "I am not supplying this" —
|
|
1843
|
+
and they let the default apply, which is what makes `id: $id` with a null variable insert a new
|
|
1844
|
+
row. A `NOT NULL` column with nothing to fill it is rejected while GraphQL coerces the request.
|
|
1845
|
+
Upserts take the create input, so they follow the insert rule.
|
|
1846
|
+
|
|
1834
1847
|
The `drizzle` block is only on errors raised from inside a field. An argument rejected while
|
|
1835
1848
|
GraphQL coerces the request — a `BigInt` variable that is not an integer, say — is rejected
|
|
1836
1849
|
before any resolver runs, so it carries its `code` but no `drizzle` and no `path`.
|
package/dist/index.cjs
CHANGED
|
@@ -29,7 +29,6 @@ let graphql = require("graphql");
|
|
|
29
29
|
let pluralize = require("pluralize");
|
|
30
30
|
pluralize = __toESM(pluralize, 1);
|
|
31
31
|
let graphql_scalars = require("graphql-scalars");
|
|
32
|
-
let graphql_parse_resolve_info = require("graphql-parse-resolve-info");
|
|
33
32
|
//#region src/util/builders/common/errors.ts
|
|
34
33
|
/**
|
|
35
34
|
* A `GraphQLError` carrying its context as data. The message says what went wrong, in prose
|
|
@@ -588,12 +587,13 @@ const remapFromGraphQLCore = (value, column, columnName) => {
|
|
|
588
587
|
return value;
|
|
589
588
|
}
|
|
590
589
|
};
|
|
591
|
-
const remapFromGraphQLSingleInput = (queryInput, table) => {
|
|
590
|
+
const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
|
|
592
591
|
for (const [key, value] of Object.entries(queryInput)) if (value === void 0) delete queryInput[key];
|
|
593
592
|
else {
|
|
594
593
|
const column = (0, drizzle_orm.getTableColumns)(table)[key];
|
|
595
594
|
if (!column) throw drizzleError(`Unknown column: ${key}`, { code: "DRIZZLE_UNKNOWN_COLUMN" });
|
|
596
595
|
if (value === null && column.notNull) {
|
|
596
|
+
if (operation === "update") throw drizzleError(`Column '${key}' cannot be set to null.`, { code: "DRIZZLE_NOT_NULL" });
|
|
597
597
|
delete queryInput[key];
|
|
598
598
|
continue;
|
|
599
599
|
}
|
|
@@ -601,8 +601,8 @@ const remapFromGraphQLSingleInput = (queryInput, table) => {
|
|
|
601
601
|
}
|
|
602
602
|
return queryInput;
|
|
603
603
|
};
|
|
604
|
-
const remapFromGraphQLArrayInput = (queryInput, table) => {
|
|
605
|
-
for (const entry of queryInput) remapFromGraphQLSingleInput(entry, table);
|
|
604
|
+
const remapFromGraphQLArrayInput = (queryInput, table, operation = "insert") => {
|
|
605
|
+
for (const entry of queryInput) remapFromGraphQLSingleInput(entry, table, operation);
|
|
606
606
|
return queryInput;
|
|
607
607
|
};
|
|
608
608
|
//#endregion
|
|
@@ -3004,10 +3004,14 @@ const splitFieldUpdateOperations = (values, table, tableName) => {
|
|
|
3004
3004
|
*
|
|
3005
3005
|
* Safe to call on every update path regardless of `features.fieldUpdateOperations` — see the
|
|
3006
3006
|
* shape check in {@link splitFieldUpdateOperations}.
|
|
3007
|
+
*
|
|
3008
|
+
* This is the single funnel every update goes through — the plural and `Single` mutations,
|
|
3009
|
+
* `updateMany`, the `Count` mutations and the MySQL builder's own — which is why it is the
|
|
3010
|
+
* one place that has to tell the remapper it is serving an update rather than an insert.
|
|
3007
3011
|
*/
|
|
3008
3012
|
const remapUpdateInput = (values, table, tableName) => {
|
|
3009
3013
|
const { columns, expressions } = splitFieldUpdateOperations(values, table, tableName);
|
|
3010
|
-
const remapped = remapFromGraphQLSingleInput(columns, table);
|
|
3014
|
+
const remapped = remapFromGraphQLSingleInput(columns, table, "update");
|
|
3011
3015
|
return Object.keys(expressions).length ? {
|
|
3012
3016
|
...remapped,
|
|
3013
3017
|
...expressions
|
|
@@ -4764,6 +4768,150 @@ const resolveBuildConfig = (config, schema) => {
|
|
|
4764
4768
|
};
|
|
4765
4769
|
};
|
|
4766
4770
|
//#endregion
|
|
4771
|
+
//#region src/util/parse-resolve-info.ts
|
|
4772
|
+
/** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
|
|
4773
|
+
const namedTypeOf = (type) => {
|
|
4774
|
+
let current = type;
|
|
4775
|
+
while (current?.ofType) current = current.ofType;
|
|
4776
|
+
return current;
|
|
4777
|
+
};
|
|
4778
|
+
/**
|
|
4779
|
+
* Whether a field's named output type has sub-selections. Object types and interfaces expose
|
|
4780
|
+
* `getFields`, unions expose `getTypes`; scalars and enums expose neither. Input object types
|
|
4781
|
+
* also expose `getFields`, but an input type can never be a field's output type, so there is
|
|
4782
|
+
* nothing here for that to collide with.
|
|
4783
|
+
*/
|
|
4784
|
+
const isCompositeLike = (type) => !!type && (typeof type.getFields === "function" || typeof type.getTypes === "function");
|
|
4785
|
+
/**
|
|
4786
|
+
* The fields declared on a type, or `undefined` when it has none to declare. A union is the
|
|
4787
|
+
* case that matters: it has no fields of its own, so a selection directly against one resolves
|
|
4788
|
+
* to nothing and is dropped — only the inline fragments underneath contribute.
|
|
4789
|
+
*/
|
|
4790
|
+
const fieldsOf = (type) => typeof type?.getFields === "function" ? type.getFields() : void 0;
|
|
4791
|
+
/**
|
|
4792
|
+
* The runtime variable values, in the plain-map form the directive check wants. graphql 17
|
|
4793
|
+
* wraps them in `{ sources, coerced }`; graphql 16 hands over the map itself.
|
|
4794
|
+
*/
|
|
4795
|
+
const coercedVariables = (info) => {
|
|
4796
|
+
const values = info.variableValues;
|
|
4797
|
+
if (values && typeof values === "object" && "coerced" in values) return values.coerced ?? {};
|
|
4798
|
+
return values ?? {};
|
|
4799
|
+
};
|
|
4800
|
+
/** The value of a `@skip` / `@include` `if:` argument — a literal or a variable reference. */
|
|
4801
|
+
const directiveIfValue = (info, value) => {
|
|
4802
|
+
if (value.kind === graphql.Kind.VARIABLE) return coercedVariables(info)[value.name.value];
|
|
4803
|
+
if (value.kind === graphql.Kind.BOOLEAN) return value.value;
|
|
4804
|
+
};
|
|
4805
|
+
/**
|
|
4806
|
+
* Whether `@skip` / `@include` on this selection exclude it. Applies to fields, fragment
|
|
4807
|
+
* spreads and inline fragments alike, which is where graphql puts them.
|
|
4808
|
+
*/
|
|
4809
|
+
const isExcluded = (info, node) => {
|
|
4810
|
+
for (const directive of node.directives ?? []) {
|
|
4811
|
+
const directiveName = directive.name.value;
|
|
4812
|
+
if (directiveName !== "skip" && directiveName !== "include") continue;
|
|
4813
|
+
const ifArgument = directive.arguments?.find((argument) => argument.name.value === "if");
|
|
4814
|
+
if (!ifArgument) continue;
|
|
4815
|
+
const value = directiveIfValue(info, ifArgument.value);
|
|
4816
|
+
if (directiveName === "skip" ? !!value : !value) return true;
|
|
4817
|
+
}
|
|
4818
|
+
return false;
|
|
4819
|
+
};
|
|
4820
|
+
/** The type a fragment's `on Type` condition names, looked up in the executing schema. */
|
|
4821
|
+
const typeFromCondition = (info, condition) => info.schema.getType(condition.name.value);
|
|
4822
|
+
/**
|
|
4823
|
+
* Introspection meta-fields are not schema fields and never reach the database. `__typename` is
|
|
4824
|
+
* the one that turns up in practice; `__id` is excluded from the rule because a schema is free
|
|
4825
|
+
* to declare a field by that name.
|
|
4826
|
+
*/
|
|
4827
|
+
const isMetaField = (name) => name.startsWith("__") && name !== "__id";
|
|
4828
|
+
/**
|
|
4829
|
+
* Merges one selection set into `tree`, which is a `fieldsByTypeName` map — the caller owns the
|
|
4830
|
+
* level, this owns the entries under `parentType`'s name.
|
|
4831
|
+
*
|
|
4832
|
+
* `activeFragments` is the set of fragment names on the current spread path. A document that
|
|
4833
|
+
* passed validation cannot contain a fragment cycle, but this parser also runs on trees a
|
|
4834
|
+
* caller assembled by hand, and an unbounded walk would hang rather than fail.
|
|
4835
|
+
*/
|
|
4836
|
+
const collectSelections = (selections, info, tree, parentType, deep, activeFragments) => {
|
|
4837
|
+
const parentTypeName = parentType?.name;
|
|
4838
|
+
if (!parentTypeName) return tree;
|
|
4839
|
+
let fields = tree[parentTypeName];
|
|
4840
|
+
if (!fields) {
|
|
4841
|
+
fields = {};
|
|
4842
|
+
tree[parentTypeName] = fields;
|
|
4843
|
+
}
|
|
4844
|
+
for (const selection of selections) {
|
|
4845
|
+
if (isExcluded(info, selection)) continue;
|
|
4846
|
+
if (selection.kind === graphql.Kind.FIELD) {
|
|
4847
|
+
const name = selection.name.value;
|
|
4848
|
+
if (isMetaField(name)) continue;
|
|
4849
|
+
const fieldDef = fieldsOf(parentType)?.[name];
|
|
4850
|
+
if (!fieldDef) continue;
|
|
4851
|
+
const fieldType = namedTypeOf(fieldDef.type);
|
|
4852
|
+
if (!fieldType) continue;
|
|
4853
|
+
const composite = isCompositeLike(fieldType);
|
|
4854
|
+
const alias = selection.alias?.value || name;
|
|
4855
|
+
let node = fields[alias];
|
|
4856
|
+
if (!node) {
|
|
4857
|
+
node = {
|
|
4858
|
+
name,
|
|
4859
|
+
alias,
|
|
4860
|
+
args: { ...(0, graphql.getArgumentValues)(fieldDef, selection, info.variableValues) },
|
|
4861
|
+
fieldsByTypeName: composite && fieldType.name ? { [fieldType.name]: {} } : {}
|
|
4862
|
+
};
|
|
4863
|
+
fields[alias] = node;
|
|
4864
|
+
}
|
|
4865
|
+
if (selection.selectionSet && deep && composite) collectSelections(selection.selectionSet.selections, info, node.fieldsByTypeName, fieldType, deep, activeFragments);
|
|
4866
|
+
continue;
|
|
4867
|
+
}
|
|
4868
|
+
if (!deep) continue;
|
|
4869
|
+
if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
|
4870
|
+
const fragmentName = selection.name.value;
|
|
4871
|
+
const fragment = info.fragments?.[fragmentName];
|
|
4872
|
+
if (!fragment) throw new Error(`Drizzle-GraphQL Error: unknown fragment '${fragmentName}' — it is spread in the query but not defined in the document.`);
|
|
4873
|
+
if (activeFragments.has(fragmentName)) continue;
|
|
4874
|
+
const fragmentType = fragment.typeCondition ? typeFromCondition(info, fragment.typeCondition) : parentType;
|
|
4875
|
+
if (!isCompositeLike(fragmentType)) continue;
|
|
4876
|
+
activeFragments.add(fragmentName);
|
|
4877
|
+
try {
|
|
4878
|
+
collectSelections(fragment.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
|
|
4879
|
+
} finally {
|
|
4880
|
+
activeFragments.delete(fragmentName);
|
|
4881
|
+
}
|
|
4882
|
+
continue;
|
|
4883
|
+
}
|
|
4884
|
+
if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
|
|
4885
|
+
const fragmentType = selection.typeCondition ? typeFromCondition(info, selection.typeCondition) : parentType;
|
|
4886
|
+
if (!isCompositeLike(fragmentType)) continue;
|
|
4887
|
+
collectSelections(selection.selectionSet.selections, info, tree, fragmentType, deep, activeFragments);
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
return tree;
|
|
4891
|
+
};
|
|
4892
|
+
const firstKey = (object) => Object.keys(object)[0];
|
|
4893
|
+
/**
|
|
4894
|
+
* Reads the selection under a resolver's own field out of its `info`, as a tree of
|
|
4895
|
+
* {@link ResolveTree} nodes.
|
|
4896
|
+
*
|
|
4897
|
+
* ```ts
|
|
4898
|
+
* const parsed = parseResolveInfo(info, { deep: true })!;
|
|
4899
|
+
* const selected = parsed.fieldsByTypeName['Users']; // keyed by response key
|
|
4900
|
+
* ```
|
|
4901
|
+
*
|
|
4902
|
+
* Returns `undefined` when the resolver's field produced no node at all, which in practice
|
|
4903
|
+
* means `info` carried no field nodes.
|
|
4904
|
+
*/
|
|
4905
|
+
const parseResolveInfo = (info, options = {}) => {
|
|
4906
|
+
const fieldNodes = info.fieldNodes;
|
|
4907
|
+
if (!fieldNodes) throw new Error("Drizzle-GraphQL Error: resolve info carries no fieldNodes, so no selection could be read from it.");
|
|
4908
|
+
const tree = collectSelections(fieldNodes, info, {}, info.parentType, options.deep ?? true, /* @__PURE__ */ new Set());
|
|
4909
|
+
const typeKey = firstKey(tree);
|
|
4910
|
+
const fields = typeKey === void 0 ? void 0 : tree[typeKey];
|
|
4911
|
+
const fieldKey = fields && firstKey(fields);
|
|
4912
|
+
return fieldKey === void 0 ? void 0 : fields?.[fieldKey];
|
|
4913
|
+
};
|
|
4914
|
+
//#endregion
|
|
4767
4915
|
//#region src/util/builders/aggregates.ts
|
|
4768
4916
|
/** Operations that aggregate over a set of column values. `count` is handled separately (whole rows). */
|
|
4769
4917
|
const AGGREGATE_OPS = [
|
|
@@ -4905,7 +5053,7 @@ const aggregateTarget = (table, tableName, typeName, resolveName) => {
|
|
|
4905
5053
|
* client asked for nothing runnable (`__typename` only, or empty sub-selections).
|
|
4906
5054
|
*/
|
|
4907
5055
|
const parseAggregateRequest = (info, target, rootTypeName = target.aggregateTypeName) => {
|
|
4908
|
-
const selectionTree =
|
|
5056
|
+
const selectionTree = parseResolveInfo(info, { deep: true }).fieldsByTypeName[rootTypeName] ?? {};
|
|
4909
5057
|
const request = {
|
|
4910
5058
|
count: false,
|
|
4911
5059
|
ops: {
|
|
@@ -5833,7 +5981,7 @@ const createSelectGenerators = (primaryKeyPropNames, nullOrdering) => {
|
|
|
5833
5981
|
resolver: async (_source, rawArgs, context, info) => {
|
|
5834
5982
|
const args = withDefaultOrderBy(rawArgs, tableName, policies?.defaultOrderBy);
|
|
5835
5983
|
try {
|
|
5836
|
-
const parsedInfo =
|
|
5984
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
5837
5985
|
const { executor, queryBase: requestQueryBase } = resolveQueryExecutor(db, context, tableName, queryBase);
|
|
5838
5986
|
return await runRelationalSelect({
|
|
5839
5987
|
queryBase: requestQueryBase,
|
|
@@ -5879,7 +6027,7 @@ const createSelectGenerators = (primaryKeyPropNames, nullOrdering) => {
|
|
|
5879
6027
|
resolver: async (_source, rawArgs, context, info) => {
|
|
5880
6028
|
const args = withDefaultOrderBy(rawArgs, tableName, policies?.defaultOrderBy);
|
|
5881
6029
|
try {
|
|
5882
|
-
const parsedInfo =
|
|
6030
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
5883
6031
|
const { executor, queryBase: requestQueryBase } = resolveQueryExecutor(db, context, tableName, queryBase);
|
|
5884
6032
|
return await runRelationalSelect({
|
|
5885
6033
|
queryBase: requestQueryBase,
|
|
@@ -6543,7 +6691,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
6543
6691
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
6544
6692
|
const scope = policies?.scope?.(context);
|
|
6545
6693
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : args.values, table), contextColumns, context);
|
|
6546
|
-
const parsedInfo =
|
|
6694
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
6547
6695
|
const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
|
|
6548
6696
|
relationMap,
|
|
6549
6697
|
tables,
|
|
@@ -6598,7 +6746,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
6598
6746
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
6599
6747
|
const scope = policies?.scope?.(context);
|
|
6600
6748
|
const input = nestedEntry ? {} : applyContextValues(remapFromGraphQLSingleInput(entry ? entry.columns : args.values, table), contextColumns, context);
|
|
6601
|
-
const parsedInfo =
|
|
6749
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
6602
6750
|
const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
|
|
6603
6751
|
relationMap,
|
|
6604
6752
|
tables,
|
|
@@ -6671,7 +6819,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
6671
6819
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
6672
6820
|
const scope = policies?.scope?.(context);
|
|
6673
6821
|
const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
|
|
6674
|
-
const parsedInfo =
|
|
6822
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
6675
6823
|
const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
|
|
6676
6824
|
relationMap,
|
|
6677
6825
|
tables,
|
|
@@ -6742,7 +6890,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
6742
6890
|
const { where, set } = args;
|
|
6743
6891
|
const scope = policies?.scope?.(context);
|
|
6744
6892
|
await before();
|
|
6745
|
-
const parsedInfo =
|
|
6893
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
6746
6894
|
const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
|
|
6747
6895
|
relationMap,
|
|
6748
6896
|
tables,
|
|
@@ -6823,7 +6971,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
|
|
|
6823
6971
|
const hard = canHardDelete && args.hard === true;
|
|
6824
6972
|
const scope = policies?.scope?.(context);
|
|
6825
6973
|
await before();
|
|
6826
|
-
const parsedInfo =
|
|
6974
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
6827
6975
|
const columns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[applyObjectTypeName(typeName, tableName, resolveName)], table, selectionCtx);
|
|
6828
6976
|
const relationCtx = relationFilterCtx(filterCtx, tableName);
|
|
6829
6977
|
const filters = withScope(scope, tableName, table, single || requireWhere ? extractRequiredFilters(table, tableName, where, relationCtx) : where ? extractFilters(table, tableName, where, relationCtx) : void 0, restore ? "ONLY" : hard ? "INCLUDE" : void 0);
|
|
@@ -7155,7 +7303,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
|
|
|
7155
7303
|
await before();
|
|
7156
7304
|
const scope = policies?.scope?.(context);
|
|
7157
7305
|
const contextColumns = policies?.contextValues?.(tableName);
|
|
7158
|
-
const parsedInfo =
|
|
7306
|
+
const parsedInfo = parseResolveInfo(info, { deep: true });
|
|
7159
7307
|
const { columns, hasRelations, withParams } = prepareMutationRelationColumns({
|
|
7160
7308
|
relationMap,
|
|
7161
7309
|
tables,
|
|
@@ -7341,7 +7489,7 @@ const namingRuleOf = (options) => resolveGeneratedTypeNames({
|
|
|
7341
7489
|
*/
|
|
7342
7490
|
const parseInfo = (info) => {
|
|
7343
7491
|
if ("fieldsByTypeName" in info) return info;
|
|
7344
|
-
return
|
|
7492
|
+
return parseResolveInfo(info, { deep: true });
|
|
7345
7493
|
};
|
|
7346
7494
|
/**
|
|
7347
7495
|
* Translates a GraphQL selection set into the relational query builder's `with` tree — the
|
|
@@ -7469,6 +7617,7 @@ exports.extractOrderBy = extractOrderBy;
|
|
|
7469
7617
|
exports.extractRelationJoinColumns = extractRelationJoinColumns;
|
|
7470
7618
|
exports.identifyRows = identifyRows;
|
|
7471
7619
|
exports.isDrizzleFieldExtension = isDrizzleFieldExtension;
|
|
7620
|
+
exports.parseResolveInfo = parseResolveInfo;
|
|
7472
7621
|
exports.resolveSelection = resolveSelection;
|
|
7473
7622
|
exports.selectionToWith = selectionToWith;
|
|
7474
7623
|
exports.singularizeMapper = singularizeMapper;
|