Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/slow-dogs-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'graphile-connection-filter': patch
'graphile-meta': patch
'graphile-search': patch
'graphile-settings': patch
---

Add CNC-owned build-state retirement with owner-specific cleanup after successful schema validation.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SQL, plans and control files are content-addressed and compared verbatim in
# tests, so the working tree must use LF on every platform.
* text=auto eol=lf

# pnpm patches retain upstream whitespace in context lines.
patches/*.patch whitespace=-blank-at-eol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ConnectionFilterCustomOperatorsPlugin } from '../src/plugins/ConnectionFilterCustomOperatorsPlugin';
import { $$filters } from '../src/types';

describe('connection-filter build-state ownership', () => {
it('clears the custom operator registry through the build lifecycle', () => {
let dispose: (() => void) | undefined;
const build: any = {
registerBuildStateDisposer(callback: () => void) {
dispose = callback;
},
};
const buildHook = ConnectionFilterCustomOperatorsPlugin.schema!.hooks!
.build as (build: any) => any;
buildHook(build);

const operators = new Map([['equalTo', { resolve: jest.fn() }]]);
build[$$filters].set('StringFilter', operators);
dispose!();

expect(operators.size).toBe(0);
expect(build[$$filters].size).toBe(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,17 @@ export const ConnectionFilterCustomOperatorsPlugin: GraphileConfig.Plugin = {
hooks: {
build(build) {
// Initialize the filter registry
build[$$filters] = new Map<
const filters = new Map<
string,
Map<string, ConnectionFilterOperatorSpec>
>();
build[$$filters] = filters;
build.registerBuildStateDisposer(() => {
for (const operators of filters.values()) {
operators.clear();
}
filters.clear();
});

return build;
},
Expand Down
18 changes: 14 additions & 4 deletions graphile/graphile-meta/__tests__/meta-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ function callGraphQLObjectTypeFieldsHook(
});
}

function callFinalizeHook(schema: GraphQLSchema, build: any): GraphQLSchema {
const finalizeHook = MetaSchemaPlugin.schema!.hooks!.finalize as (
schema: GraphQLSchema,
build: any
) => GraphQLSchema;
return finalizeHook(schema, build);
}

function deepClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
Expand Down Expand Up @@ -2016,6 +2024,7 @@ describe('MetaSchemaPlugin', () => {
}),
types: [userType]
});
callFinalizeHook(schema, build);
const result = await graphql({
schema,
source: `
Expand Down Expand Up @@ -2086,6 +2095,7 @@ describe('MetaSchemaPlugin', () => {
})
]
});
callFinalizeHook(schema, build);
return schema;
};

Expand All @@ -2112,7 +2122,7 @@ describe('MetaSchemaPlugin', () => {
).toEqual(['Project']);
});

it('validates metadata against schema changes made by later finalizers', async () => {
it('snapshots metadata from the finalized executable schema', async () => {
const codec = createMockCodec('user', {
id: createMockAttribute('text')
});
Expand Down Expand Up @@ -2144,11 +2154,11 @@ describe('MetaSchemaPlugin', () => {
});
const schema = new GraphQLSchema({ query: queryType });

// Before later finalizers mutate the schema, the metadata resolves the
// list entry-point; the resolver must recompute from the final schema.
// Simulate an earlier finalizer removing an entry-point before the meta
// plugin snapshots the executable schema.
expect((collectTablesMeta(build, schema) as any[])[0].query.all).toBe('users');

delete queryType.getFields().users;
callFinalizeHook(schema, build);
const result = await graphql({
schema,
source: '{ _meta { tables { query { all } } } }'
Expand Down
23 changes: 12 additions & 11 deletions graphile/graphile-meta/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,19 @@ import type { MetaBuild, TableMeta } from './types';

const runtimeTablesBySchema = new WeakMap<GraphQLSchema, TableMeta[]>();

function getRuntimeTablesMeta(
build: MetaBuild,
schema: GraphQLSchema
): TableMeta[] {
let tables = runtimeTablesBySchema.get(schema);
function getRuntimeTablesMeta(schema: GraphQLSchema): TableMeta[] {
const tables = runtimeTablesBySchema.get(schema);
if (!tables) {
tables = collectTablesMeta(build, schema);
runtimeTablesBySchema.set(schema, tables);
throw new Error(
'Meta schema runtime state was not finalized for this GraphQL schema'
);
}
return tables;
}

/**
* Returns the table metadata memoized for the given executable schema, or
* `undefined` if `_meta` has not been resolved against that schema (e.g. the
* meta plugin is disabled or `_meta` was never executed).
* `undefined` when the meta plugin was not installed for that schema.
*/
export function getTablesMetaForSchema(
schema: GraphQLSchema
Expand All @@ -41,12 +38,16 @@ export const MetaSchemaPlugin: GraphileConfig.Plugin = {
hooks: {
GraphQLObjectType_fields(rawFields, rawBuild, rawContext) {
if (!rawContext.scope.isRootQuery) return rawFields;
const build = rawBuild as unknown as MetaBuild;
return extendQueryWithMetaField(
rawFields as unknown as Record<string, unknown>,
(schema) => getRuntimeTablesMeta(build, schema),
getRuntimeTablesMeta
) as typeof rawFields;
},
finalize(schema, rawBuild) {
const build = rawBuild as unknown as MetaBuild;
runtimeTablesBySchema.set(schema, collectTablesMeta(build, schema));
return schema;
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createUnifiedSearchPlugin } from '../plugin';

function getBuildHook(plugin: GraphileConfig.Plugin): (build: any) => any {
return plugin.schema!.hooks!.build as (build: any) => any;
}

describe('graphile-search build-state ownership', () => {
it('clears the unified-search codec cache through the build lifecycle', () => {
const detectColumns = jest.fn((): never[] => []);
const plugin = createUnifiedSearchPlugin({
adapters: [
{
name: 'test',
detectColumns,
registerTypes: jest.fn(),
scoreSemantics: { metric: 'score', lowerIsBetter: false },
} as never,
],
});
let dispose: (() => void) | undefined;
const build = {
registerBuildStateDisposer(callback: () => void) {
dispose = callback;
},
};
getBuildHook(plugin)(build);

const inferred = (plugin.schema!.entityBehavior!.pgCodecAttribute as any)
.inferred.callback;
const codec = { name: 'document', attributes: { body: {} } };
inferred([], [codec, 'body'], build);
inferred([], [codec, 'body'], build);
expect(detectColumns).toHaveBeenCalledTimes(1);

dispose!();
inferred([], [codec, 'body'], build);
expect(detectColumns).toHaveBeenCalledTimes(2);
});
});
5 changes: 5 additions & 0 deletions graphile/graphile-search/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ export function createUnifiedSearchPlugin(
},

hooks: {
build(build) {
build.registerBuildStateDisposer(() => codecCache.clear());
return build;
},

/**
* Register all adapter-specific GraphQL types during init.
*/
Expand Down
Loading