Skip to content

Commit ae2315e

Browse files
authored
'graphile behavior debug' & fix issue with badly scoped behaviors (#410)
2 parents a472525 + a2b3e88 commit ae2315e

File tree

18 files changed

+469
-180
lines changed

18 files changed

+469
-180
lines changed

.changeset/three-coats-exist.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"graphile-build-pg": patch
3+
"graphile-build": patch
4+
"graphile-utils": patch
5+
"postgraphile": patch
6+
---
7+
8+
Use a single behavior check per location.
9+
10+
In the past two weeks I added a few behavior strings like
11+
`array:attribute:filterBy` (a scoped form of `attribute:filterBy` to only be
12+
used by attributes that were arrays); however I've realised that this will
13+
require plugin authors to implement all the same logic to figure out what type
14+
an attribute is in order to then see if it has the relevant behavior. This goes
15+
against the design of the behavior system, and makes plugin authors' lives
16+
harder. So I've reverted this, and instead used the `entityBehaviors` system to
17+
add or remove the base `attribute:filterBy` (etc) behavior depending on what the
18+
type of the attribute is.

.changeset/young-kiwis-roll.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"graphile": patch
3+
---
4+
5+
Add new `graphile behavior debug` command for debugging behavior strings.

graphile-build/graphile-build-pg/src/plugins/PgAttributesPlugin.ts

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
PgCodecAttribute,
99
PgCodecAttributes,
1010
PgCodecList,
11-
PgCodecRelation,
1211
PgCodecWithAttributes,
1312
PgConditionStep,
1413
PgSelectSingleStep,
@@ -294,7 +293,42 @@ export const PgAttributesPlugin: GraphileConfig.Plugin = {
294293

295294
schema: {
296295
entityBehavior: {
297-
pgCodecAttribute: "select base update insert",
296+
pgCodecAttribute: {
297+
provides: ["default"],
298+
before: ["inferred", "override"],
299+
callback(behavior, [codec, attributeName]) {
300+
const behaviors = new Set([
301+
"select base update insert filterBy orderBy",
302+
]);
303+
const attribute = codec.attributes[attributeName];
304+
function walk(codec: PgCodec) {
305+
if (codec.arrayOfCodec) {
306+
behaviors.add("-condition:attribute:filterBy");
307+
behaviors.add(`-attribute:orderBy`);
308+
walk(codec.arrayOfCodec);
309+
} else if (codec.rangeOfCodec) {
310+
behaviors.add(`-condition:attribute:filterBy`);
311+
behaviors.add(`-attribute:orderBy`);
312+
walk(codec.rangeOfCodec);
313+
} else if (codec.domainOfCodec) {
314+
// No need to add a behavior for domain
315+
walk(codec.domainOfCodec);
316+
} else if (codec.attributes) {
317+
behaviors.add(`-condition:attribute:filterBy`);
318+
behaviors.add(`-attribute:orderBy`);
319+
} else if (codec.isBinary) {
320+
// Never filter, not in condition plugin nor any other
321+
behaviors.add(`-attribute:filterBy`);
322+
behaviors.add(`-attribute:orderBy`);
323+
} else {
324+
// Done
325+
}
326+
}
327+
walk(attribute.codec);
328+
329+
return [...behaviors, behavior];
330+
},
331+
},
298332
},
299333
hooks: {
300334
GraphQLInterfaceType_fields(fields, build, context) {
@@ -384,12 +418,7 @@ export const PgAttributesPlugin: GraphileConfig.Plugin = {
384418
return fields;
385419
},
386420
GraphQLInputObjectType_fields(fields, build, context) {
387-
const {
388-
extend,
389-
inflection,
390-
input: { pgRegistry },
391-
sql,
392-
} = build;
421+
const { extend, inflection, sql } = build;
393422
const {
394423
scope: {
395424
isPgRowType,
@@ -440,56 +469,21 @@ export const PgAttributesPlugin: GraphileConfig.Plugin = {
440469
return Object.entries(attributes).reduce(
441470
(memo, [attributeName, attribute]) =>
442471
build.recoverable(memo, () => {
443-
const action = isPgBaseInput
444-
? "base"
472+
const fieldBehaviorScope = isPgBaseInput
473+
? `attribute:base`
445474
: isPgPatch
446-
? "update"
475+
? `attribute:update`
447476
: isPgCondition
448-
? "filterBy"
449-
: "insert";
450-
451-
const behaviors: string[] = [];
452-
function walk(codec: PgCodec) {
453-
if (codec.arrayOfCodec) {
454-
behaviors.push(`array:attribute:${action}`);
455-
walk(codec.arrayOfCodec);
456-
} else if (codec.rangeOfCodec) {
457-
behaviors.push(`range:attribute:${action}`);
458-
walk(codec.rangeOfCodec);
459-
} else if (codec.domainOfCodec) {
460-
// No need to add a behavior for domain
461-
walk(codec.domainOfCodec);
462-
} else if (codec.attributes) {
463-
behaviors.push(`composite:attribute:${action}`);
464-
} else if (codec.isBinary) {
465-
behaviors.push(`binary:attribute:${action}`);
466-
} else {
467-
behaviors.push(`scalar:attribute:${action}`);
468-
}
469-
}
470-
walk(attribute.codec);
471-
472-
const relations = (
473-
Object.values(
474-
pgRegistry.pgRelations[pgCodec.name] ?? {},
475-
) as PgCodecRelation[]
476-
).filter((r) => !r.isReferencee && r.isUnique);
477-
const isPartOfRelation = relations.some((r) =>
478-
r.localAttributes.includes(attributeName),
479-
);
480-
if (isPartOfRelation) {
481-
behaviors.push(`relation:attribute:${action}`);
482-
}
483-
484-
for (const behavior of behaviors) {
485-
if (
486-
!build.behavior.pgCodecAttributeMatches(
487-
[pgCodec, attributeName],
488-
behavior,
489-
)
490-
) {
491-
return memo;
492-
}
477+
? `condition:attribute:filterBy`
478+
: `attribute:insert`;
479+
480+
if (
481+
!build.behavior.pgCodecAttributeMatches(
482+
[pgCodec, attributeName],
483+
fieldBehaviorScope,
484+
)
485+
) {
486+
return memo;
493487
}
494488

495489
const fieldName = inflection.attribute({
@@ -514,7 +508,7 @@ export const PgAttributesPlugin: GraphileConfig.Plugin = {
514508
[fieldName]: fieldWithHooks(
515509
{
516510
fieldName,
517-
fieldBehaviorScope: `attribute:${action}`,
511+
fieldBehaviorScope,
518512
pgCodec,
519513
pgAttribute: attribute,
520514
isPgConnectionConditionInputField: isPgCondition,

graphile-build/graphile-build-pg/src/plugins/PgConditionArgumentPlugin.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export const PgConditionArgumentPlugin: GraphileConfig.Plugin = {
4242
schema: {
4343
entityBehavior: {
4444
pgCodec: "select filter",
45-
pgCodecAttribute: "filterBy -binary:attribute:filterBy",
4645
pgResource: {
4746
provides: ["default"],
4847
before: ["inferred", "override"],

graphile-build/graphile-build-pg/src/plugins/PgOrderAllAttributesPlugin.ts

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import "./PgTablesPlugin.js";
22
import "graphile-config";
33

44
import type {
5-
PgCodec,
65
PgCodecAttribute,
76
PgCodecWithAttributes,
87
PgResourceUnique,
@@ -45,11 +44,6 @@ export const PgOrderAllAttributesPlugin: GraphileConfig.Plugin = {
4544
},
4645

4746
schema: {
48-
entityBehavior: {
49-
// Enable ordering, but don't order by array, range or binary types
50-
pgCodecAttribute:
51-
"orderBy -array:attribute:orderBy -range:attribute:orderBy -binary:attribute:orderBy",
52-
},
5347
hooks: {
5448
GraphQLEnumType_values(values, build, context) {
5549
const { extend, inflection, options } = build;
@@ -101,35 +95,14 @@ export const PgOrderAllAttributesPlugin: GraphileConfig.Plugin = {
10195
values,
10296
Object.entries(attributes).reduce(
10397
(memo, [attributeName, attribute]) => {
104-
const behaviors: string[] = [];
105-
function walk(codec: PgCodec) {
106-
if (codec.arrayOfCodec) {
107-
behaviors.push("array:attribute:orderBy");
108-
walk(codec.arrayOfCodec);
109-
} else if (codec.rangeOfCodec) {
110-
behaviors.push("range:attribute:orderBy");
111-
walk(codec.rangeOfCodec);
112-
} else if (codec.domainOfCodec) {
113-
// No need to add a behavior for domain
114-
walk(codec.domainOfCodec);
115-
} else if (codec.attributes) {
116-
behaviors.push("composite:attribute:orderBy");
117-
} else if (codec.isBinary) {
118-
behaviors.push("binary:attribute:orderBy");
119-
} else {
120-
behaviors.push("scalar:attribute:orderBy");
121-
}
122-
}
123-
walk(attribute.codec);
124-
for (const behavior of behaviors) {
125-
if (
126-
!build.behavior.pgCodecAttributeMatches(
127-
[pgCodec, attributeName],
128-
behavior,
129-
)
130-
) {
131-
return memo;
132-
}
98+
const fieldBehaviorScope = `attribute:orderBy`;
99+
if (
100+
!build.behavior.pgCodecAttributeMatches(
101+
[pgCodec, attributeName],
102+
fieldBehaviorScope,
103+
)
104+
) {
105+
return memo;
133106
}
134107
const isUnique = uniques.some(
135108
(list) => list.attributes[0] === attributeName,

graphile-build/graphile-build/src/behavior.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export class Behavior {
7979
};
8080
};
8181

82+
public behaviorEntityTypes: (keyof GraphileBuild.BehaviorEntities)[] = [];
83+
8284
private globalDefaultBehavior: ResolvedBehavior;
8385
constructor(
8486
private resolvedPreset: GraphileConfig.ResolvedPreset,
@@ -152,6 +154,7 @@ export class Behavior {
152154
(this as this & BehaviorDynamicMethods).stringBehavior = (str) => str;
153155
return;
154156
}
157+
this.behaviorEntityTypes.push(entityType);
155158
this.behaviorEntities[entityType] = {
156159
behaviorCallbacks: [],
157160
listCache: new Map(),
@@ -274,6 +277,14 @@ export class Behavior {
274277
} ${specString ?? ""}`;
275278
return stringMatches(finalBehaviorSpecsString, filter);
276279
}
280+
281+
parseBehaviorString(behaviorString: string) {
282+
return parseSpecs(behaviorString);
283+
}
284+
285+
parseScope(filter: string) {
286+
return parseScope(filter);
287+
}
277288
}
278289

279290
/**
@@ -295,6 +306,7 @@ function parseSpecs(behaviorSpecsString: string): BehaviorSpec[] {
295306
const fragments = behaviorSpecsString.split(/\s+/);
296307
const specs: BehaviorSpec[] = [];
297308
for (const fragment of fragments) {
309+
if (fragment === "") continue;
298310
// `+` is implicit
299311
const [pm, rest] = /^[+-]/.test(fragment)
300312
? [fragment.slice(0, 1), fragment.slice(1)]
@@ -323,6 +335,10 @@ function parseSpecs(behaviorSpecsString: string): BehaviorSpec[] {
323335
function scopeMatches(
324336
specifiedScope: BehaviorScope,
325337
filterScope: BehaviorScope,
338+
// We only need to know positive or negative in case the filter contains an `*`.
339+
// This is because if you filter for '*:foo' against '+bar:foo -baz:foo' then
340+
// we should skip the negative (`-baz:foo`) because we only need _one_ match,
341+
// not *every* match.
326342
positive: boolean,
327343
): boolean {
328344
if (specifiedScope.length > filterScope.length) {

0 commit comments

Comments
 (0)