diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index ece96f4ed5..76db5cc002 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -246,7 +246,7 @@ jobs: - batch: pg-graphql packages: 'graphile/graphile-search graphile/graphile-ltree graphile/graphile-bulk-mutations graphile/graphile-function-bindings graphile/graphile-history graphile/graphile-meta graphile/graphile-schema graphql/orm-test graphql/test graphql/playwright-test' - batch: pg-graphile-extras - packages: 'graphile/graphile-i18n graphile/graphile-pg-aggregates graphile/graphile-query graphile/graphile-realtime-test' + packages: 'graphile/graphile-cache graphile/graphile-i18n graphile/graphile-pg-aggregates graphile/graphile-query graphile/graphile-realtime-test' env: PGHOST: localhost diff --git a/graphile/graphile-cache/jest.config.js b/graphile/graphile-cache/jest.config.js index 057a9420ed..df73257b5a 100644 --- a/graphile/graphile-cache/jest.config.js +++ b/graphile/graphile-cache/jest.config.js @@ -7,7 +7,8 @@ module.exports = { 'ts-jest', { babelConfig: false, - tsconfig: 'tsconfig.json', + // ts-jest requires isolated transformation for NodeNext package exports. + tsconfig: { isolatedModules: true }, }, ], }, diff --git a/graphile/graphile-cache/package.json b/graphile/graphile-cache/package.json index d158a99c8a..a7c1838bc8 100644 --- a/graphile/graphile-cache/package.json +++ b/graphile/graphile-cache/package.json @@ -42,6 +42,7 @@ "@types/express": "^5.0.6", "makage": "^0.8.0", "nodemon": "^3.1.14", + "pgsql-test": "workspace:^", "ts-node": "^10.9.2" }, "keywords": [ diff --git a/graphile/graphile-cache/src/__tests__/build-readiness.test.ts b/graphile/graphile-cache/src/__tests__/build-readiness.test.ts new file mode 100644 index 0000000000..caaf7a5a44 --- /dev/null +++ b/graphile/graphile-cache/src/__tests__/build-readiness.test.ts @@ -0,0 +1,164 @@ +import { awaitGraphileBuildReadiness } from '../build-readiness'; + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: Error): void; +} + +const deferred = (): Deferred => { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +}; + +const flushPromises = (): Promise => + new Promise((resolve) => setImmediate(resolve)); + +describe('awaitGraphileBuildReadiness', () => { + it('does not resolve before schema gathering and Grafserv are ready', async () => { + const schemaResult = deferred(); + const ready = deferred(); + const release = jest.fn().mockResolvedValue(undefined); + let resolved = false; + const buildPromise = awaitGraphileBuildReadiness({ + schemaResult: schemaResult.promise, + addTo: jest.fn().mockResolvedValue(undefined), + ready: () => ready.promise, + release, + }).then(() => { + resolved = true; + }); + + schemaResult.resolve({}); + await flushPromises(); + expect(resolved).toBe(false); + + ready.resolve(undefined); + await buildPromise; + expect(release).not.toHaveBeenCalled(); + }); + + it('does not start readiness checks before the adapter is attached', async () => { + const addTo = deferred(); + const ready = jest.fn().mockResolvedValue(undefined); + const buildPromise = awaitGraphileBuildReadiness({ + schemaResult: Promise.resolve({}), + addTo: () => addTo.promise, + ready, + release: jest.fn().mockResolvedValue(undefined), + }); + + await flushPromises(); + expect(ready).not.toHaveBeenCalled(); + + addTo.resolve(undefined); + await buildPromise; + expect(ready).toHaveBeenCalledTimes(1); + }); + + it('waits for schema construction before releasing after adapter attachment fails', async () => { + const schemaResult = deferred(); + const attachmentFailure = new Error('adapter attachment failed'); + const release = jest.fn().mockResolvedValue(undefined); + const buildPromise = awaitGraphileBuildReadiness({ + schemaResult: schemaResult.promise, + addTo: jest.fn().mockRejectedValue(attachmentFailure), + ready: jest.fn().mockResolvedValue(undefined), + release, + }); + + await flushPromises(); + expect(release).not.toHaveBeenCalled(); + + schemaResult.resolve({}); + await expect(buildPromise).rejects.toBe(attachmentFailure); + expect(release).toHaveBeenCalledTimes(1); + }); + + it('waits for schema construction before releasing after readiness fails', async () => { + const schemaResult = deferred(); + const readinessFailure = new Error('grafserv readiness failed'); + const release = jest.fn().mockResolvedValue(undefined); + const buildPromise = awaitGraphileBuildReadiness({ + schemaResult: schemaResult.promise, + addTo: jest.fn().mockResolvedValue(undefined), + ready: jest.fn().mockRejectedValue(readinessFailure), + release, + }); + + await flushPromises(); + expect(release).not.toHaveBeenCalled(); + + schemaResult.resolve({}); + await expect(buildPromise).rejects.toBe(readinessFailure); + expect(release).toHaveBeenCalledTimes(1); + }); + + it('observes schema failure while adapter attachment is pending', async () => { + const schemaResult = deferred(); + const addTo = deferred(); + const release = jest.fn().mockResolvedValue(undefined); + const failure = new Error('schema build failed early'); + const buildPromise = awaitGraphileBuildReadiness({ + schemaResult: schemaResult.promise, + addTo: () => addTo.promise, + ready: jest.fn().mockResolvedValue(undefined), + release, + }); + + schemaResult.reject(failure); + await flushPromises(); + expect(release).not.toHaveBeenCalled(); + + addTo.resolve(undefined); + await expect(buildPromise).rejects.toBe(failure); + expect(release).toHaveBeenCalledTimes(1); + }); + + it('awaits failed-generation release before rejecting', async () => { + const schemaResult = deferred(); + const release = deferred(); + const releaseFn = jest.fn(() => release.promise); + const failure = new Error('schema build failed'); + let rejected = false; + const buildPromise = awaitGraphileBuildReadiness({ + schemaResult: schemaResult.promise, + addTo: jest.fn().mockResolvedValue(undefined), + ready: jest.fn().mockResolvedValue(undefined), + release: releaseFn, + }).catch((error) => { + rejected = true; + throw error; + }); + + schemaResult.reject(failure); + await flushPromises(); + expect(releaseFn).toHaveBeenCalledTimes(1); + expect(rejected).toBe(false); + + release.resolve(undefined); + await expect(buildPromise).rejects.toBe(failure); + }); + + it('preserves the build failure when cleanup also fails', async () => { + const failure = new Error('schema build failed'); + const cleanupFailure = new Error('release failed'); + const onReleaseError = jest.fn(); + + await expect( + awaitGraphileBuildReadiness({ + schemaResult: Promise.reject(failure), + addTo: jest.fn().mockResolvedValue(undefined), + ready: jest.fn().mockResolvedValue(undefined), + release: jest.fn().mockRejectedValue(cleanupFailure), + onReleaseError, + }) + ).rejects.toBe(failure); + expect(onReleaseError).toHaveBeenCalledWith(cleanupFailure); + }); +}); diff --git a/graphile/graphile-cache/src/__tests__/disposal-lifecycle.test.ts b/graphile/graphile-cache/src/__tests__/disposal-lifecycle.test.ts new file mode 100644 index 0000000000..d187469bd5 --- /dev/null +++ b/graphile/graphile-cache/src/__tests__/disposal-lifecycle.test.ts @@ -0,0 +1,241 @@ +jest.mock('@pgpmjs/logger', () => ({ + Logger: jest.fn(() => ({ + debug: jest.fn(), + error: jest.fn(), + })), +})); + +jest.mock('pg-cache', () => ({ + pgCache: { + registerCleanupCallback: jest.fn(() => jest.fn()), + close: jest.fn().mockResolvedValue(undefined), + }, +})); + +import type { GraphileCacheEntry } from '../graphile-cache'; +import { + assertGraphileCacheOpen, + clearGraphileCache, + clearMatchingEntries, + closeAllCaches, + disposeUncachedEntry, + graphileCache, + trackGraphileBuild, + waitForEntryDisposal, +} from '../graphile-cache'; + +interface Deferred { + promise: Promise; + resolve(value: T): void; +} + +const deferred = (): Deferred => { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +}; + +const flushPromises = (): Promise => + new Promise((resolve) => setImmediate(resolve)); + +const makeEntry = ( + cacheKey: string, + release = jest.fn().mockResolvedValue(undefined), + releasePresetServices = jest.fn().mockResolvedValue(undefined), + releaseRuntimePool = jest.fn().mockResolvedValue(undefined) +): GraphileCacheEntry => + ({ + pgl: { release }, + serv: {}, + handler: {}, + httpServer: { listening: false }, + cacheKey, + createdAt: Date.now(), + releasePresetServices, + releaseRuntimePool, + }) as unknown as GraphileCacheEntry; + +describe('Graphile cache disposal lifecycle', () => { + afterEach(async () => { + await clearGraphileCache(); + }); + + it('coalesces concurrent disposal of one exact entry', async () => { + const release = jest.fn().mockResolvedValue(undefined); + const releasePresetServices = jest.fn().mockResolvedValue(undefined); + const entry = makeEntry('same-entry', release, releasePresetServices); + + const first = disposeUncachedEntry(entry); + const second = disposeUncachedEntry(entry); + + expect(second).toBe(first); + await Promise.all([first, second]); + expect(release).toHaveBeenCalledTimes(1); + expect(releasePresetServices).toHaveBeenCalledTimes(1); + }); + + it('disposes distinct generations that reuse the same cache key', async () => { + const firstRelease = jest.fn().mockResolvedValue(undefined); + const secondRelease = jest.fn().mockResolvedValue(undefined); + const first = makeEntry('shared-key', firstRelease); + const second = makeEntry('shared-key', secondRelease); + + await Promise.all([ + disposeUncachedEntry(first), + disposeUncachedEntry(second), + ]); + + expect(firstRelease).toHaveBeenCalledTimes(1); + expect(secondRelease).toHaveBeenCalledTimes(1); + }); + + it('continues cleanup and exposes the first disposal failure', async () => { + const failure = new Error('realtime stop failed'); + const release = jest.fn().mockResolvedValue(undefined); + const releasePresetServices = jest.fn().mockResolvedValue(undefined); + const entry = makeEntry('failed-cleanup', release, releasePresetServices); + entry.realtimeManager = { stop: jest.fn().mockRejectedValue(failure) }; + + await expect(disposeUncachedEntry(entry)).rejects.toBe(failure); + expect(release).toHaveBeenCalledTimes(1); + expect(releasePresetServices).toHaveBeenCalledTimes(1); + }); + + it('lets callers await an eviction through the exact entry', async () => { + const release = deferred(); + const releasePresetServices = jest.fn().mockResolvedValue(undefined); + const entry = makeEntry( + 'evicted-entry', + jest.fn(() => release.promise), + releasePresetServices + ); + graphileCache.set(entry.cacheKey, entry); + graphileCache.delete(entry.cacheKey); + + let disposed = false; + const waiting = waitForEntryDisposal(entry).then(() => { + disposed = true; + }); + await flushPromises(); + expect(disposed).toBe(false); + + release.resolve(undefined); + await waiting; + expect(disposed).toBe(true); + expect(releasePresetServices).toHaveBeenCalledTimes(1); + }); + + it('does not resolve a cache clear before resident disposal completes', async () => { + const release = deferred(); + const entry = makeEntry( + 'clear-entry', + jest.fn(() => release.promise) + ); + graphileCache.set(entry.cacheKey, entry); + + let cleared = false; + const clearing = clearGraphileCache().then(() => { + cleared = true; + }); + await flushPromises(); + expect(graphileCache.size).toBe(0); + expect(cleared).toBe(false); + + release.resolve(undefined); + await clearing; + expect(cleared).toBe(true); + }); + + it('does not complete entry disposal before preset services are released', async () => { + const releasePresetServices = deferred(); + const entry = makeEntry( + 'delayed-preset-service', + jest.fn().mockResolvedValue(undefined), + jest.fn(() => releasePresetServices.promise) + ); + graphileCache.set(entry.cacheKey, entry); + + graphileCache.delete(entry.cacheKey); + const disposal = waitForEntryDisposal(entry); + let disposed = false; + void disposal.then(() => { + disposed = true; + }); + + await flushPromises(); + expect(disposed).toBe(false); + + releasePresetServices.resolve(undefined); + await disposal; + expect(disposed).toBe(true); + }); + + it('disposes a new generation after an older same-key generation fails', async () => { + const firstFailure = new Error('first generation release failed'); + const firstRelease = jest.fn().mockRejectedValue(firstFailure); + const secondRelease = jest.fn().mockResolvedValue(undefined); + const first = makeEntry('retry-by-generation', firstRelease); + const second = makeEntry('retry-by-generation', secondRelease); + + await expect(disposeUncachedEntry(first)).rejects.toBe(firstFailure); + await expect(disposeUncachedEntry(second)).resolves.toBeUndefined(); + + expect(firstRelease).toHaveBeenCalledTimes(1); + expect(secondRelease).toHaveBeenCalledTimes(1); + }); + + it('releases the runtime pool after services even when service cleanup fails', async () => { + const serviceFailure = new Error('service release failed'); + const events: string[] = []; + const release = jest.fn(async () => { + events.push('pgl'); + }); + const releasePresetServices = jest.fn(async () => { + events.push('services'); + throw serviceFailure; + }); + const releaseRuntimePool = jest.fn(async () => { + events.push('runtime-pool'); + }); + const entry = makeEntry( + 'release-order', + release, + releasePresetServices, + releaseRuntimePool + ); + + await expect(disposeUncachedEntry(entry)).rejects.toBe(serviceFailure); + expect(events).toEqual(['pgl', 'services', 'runtime-pool']); + expect(releaseRuntimePool).toHaveBeenCalledTimes(1); + }); + + it('matches targeted clears against logical service keys', async () => { + const entry = makeEntry('opaque-pool-key:one'); + entry.logicalServiceKey = 'service:one'; + graphileCache.set(entry.cacheKey, entry); + + expect(clearMatchingEntries(/^service:/)).toBe(1); + await waitForEntryDisposal(entry); + }); + + it('blocks new builds during close and disposes a build that finishes then', async () => { + const build = deferred(); + const release = jest.fn().mockResolvedValue(undefined); + const entry = makeEntry('closing-build', release); + const tracked = trackGraphileBuild(() => build.promise); + const closing = closeAllCaches(); + + expect(() => assertGraphileCacheOpen()).toThrow('Graphile cache is closing'); + expect(() => trackGraphileBuild(() => Promise.resolve(entry))).toThrow( + 'Graphile cache is closing' + ); + + build.resolve(entry); + await expect(tracked).rejects.toThrow('Graphile cache is closing'); + await closing; + expect(release).toHaveBeenCalledTimes(1); + expect(() => assertGraphileCacheOpen()).not.toThrow(); + }); +}); diff --git a/graphile/graphile-cache/src/__tests__/pg-service-release.test.ts b/graphile/graphile-cache/src/__tests__/pg-service-release.test.ts new file mode 100644 index 0000000000..fc13d516f5 --- /dev/null +++ b/graphile/graphile-cache/src/__tests__/pg-service-release.test.ts @@ -0,0 +1,264 @@ +import { EventEmitter } from 'node:events'; + +import { makePgService, PgSubscriber } from 'postgraphile/adaptors/pg'; +import { getConnections } from 'pgsql-test'; + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: unknown): void; +} + +const deferred = (): Deferred => { + let resolve!: (value: T) => void; + let reject!: (error: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +}; + +const flushPromises = (): Promise => + new Promise((resolve) => setImmediate(resolve)); + +const waitFor = async (predicate: () => boolean): Promise => { + for (let attempt = 0; attempt < 20; attempt++) { + if (predicate()) return; + await flushPromises(); + } + throw new Error('Timed out waiting for the fake Postgres boundary'); +}; + +class FakeClient extends EventEmitter { + readonly queryCalls: string[] = []; + readonly releaseCalls: boolean[] = []; + queryHandler: (text: string) => Promise = async () => undefined; + + query(text: string): Promise { + this.queryCalls.push(text); + return this.queryHandler(text); + } + + escapeIdentifier(identifier: string): string { + return `"${identifier}"`; + } + + release(destroy?: Error | boolean): void { + this.releaseCalls.push(destroy === true); + } +} + +type SubscriberPool = ConstructorParameters[0]; + +const makePool = ( + connect: () => Promise +): { + pool: SubscriberPool; + connect: jest.Mock, []>; +} => { + const connectMock = jest.fn(connect); + return { + pool: { connect: connectMock } as unknown as SubscriberPool, + connect: connectMock, + }; +}; + +describe('public PostGraphile PostgreSQL release contracts', () => { + it('waits for UNLISTEN and memoizes repeated subscriber release calls', async () => { + const client = new FakeClient(); + const unlisten = deferred(); + client.queryHandler = async (text) => { + if (text.startsWith('UNLISTEN')) return unlisten.promise; + return undefined; + }; + const { pool } = makePool(async () => client); + const subscriber = new PgSubscriber(pool); + subscriber.subscribe('topic'); + + await waitFor(() => client.queryCalls.some((text) => text.startsWith('LISTEN'))); + + const firstRelease = subscriber.release(); + const secondRelease = subscriber.release(); + expect(secondRelease).toBe(firstRelease); + + await waitFor(() => + client.queryCalls.some((text) => text.startsWith('UNLISTEN')) + ); + expect(client.releaseCalls).toEqual([]); + + unlisten.resolve(undefined); + await firstRelease; + expect(client.releaseCalls).toEqual([false]); + expect(client.listenerCount('notification')).toBe(0); + expect(client.listenerCount('error')).toBe(0); + }); + + it('finishes every pending iterator for one topic during release', async () => { + const client = new FakeClient(); + const { pool } = makePool(async () => client); + const subscriber = new PgSubscriber(pool); + const first = subscriber.subscribe('topic'); + const second = subscriber.subscribe('topic'); + const firstNext = first.next(); + const secondNext = second.next(); + + await subscriber.release(); + await expect(firstNext).resolves.toMatchObject({ done: true }); + await expect(secondNext).resolves.toMatchObject({ done: true }); + }); + + it('destroys a client that resolves after shutdown starts', async () => { + const client = new FakeClient(); + const connection = deferred(); + const { pool, connect } = makePool(() => connection.promise); + const subscriber = new PgSubscriber(pool); + subscriber.subscribe('topic'); + await waitFor(() => connect.mock.calls.length === 1); + + const release = subscriber.release(); + await flushPromises(); + expect(client.releaseCalls).toEqual([]); + + connection.resolve(client); + await release; + expect(client.releaseCalls).toEqual([true]); + expect(client.queryCalls).toEqual([]); + expect(client.listenerCount('notification')).toBe(0); + expect(client.listenerCount('error')).toBe(0); + }); + + it('surfaces a pending connection failure during release', async () => { + const connectionFailure = new Error('connection failed during shutdown'); + const connection = deferred(); + const { pool, connect } = makePool(() => connection.promise); + const subscriber = new PgSubscriber(pool); + subscriber.subscribe('topic'); + await waitFor(() => connect.mock.calls.length === 1); + + const release = subscriber.release(); + connection.reject(connectionFailure); + + await expect(release).rejects.toBe(connectionFailure); + }); + + it('destroys the client and surfaces an UNLISTEN failure', async () => { + const client = new FakeClient(); + const unlistenFailure = new Error('UNLISTEN failed'); + client.queryHandler = async (text) => { + if (text.startsWith('UNLISTEN')) throw unlistenFailure; + return undefined; + }; + const { pool } = makePool(async () => client); + const subscriber = new PgSubscriber(pool); + subscriber.subscribe('topic'); + + await waitFor(() => client.queryCalls.some((text) => text.startsWith('LISTEN'))); + await expect(subscriber.release()).rejects.toBe(unlistenFailure); + + expect(client.releaseCalls).toEqual([true]); + expect(client.listenerCount('notification')).toBe(0); + expect(client.listenerCount('error')).toBe(0); + }); + + it('attempts every owned service releaser and preserves ordered failures', async () => { + const subscriberFailure = new Error('subscriber release failed'); + const poolFailure = new Error('pool end failed'); + const events: string[] = []; + const service = makePgService({ + connectionString: 'postgres://unused/release-contract', + schemas: ['public'], + }); + const subscriber = service.pgSubscriber as unknown as { + release: () => Promise; + }; + const pool = service.adaptorSettings?.pool as unknown as { + end: () => Promise; + }; + subscriber.release = jest.fn(async () => { + events.push('subscriber'); + throw subscriberFailure; + }); + pool.end = jest.fn(async () => { + events.push('pool'); + throw poolFailure; + }); + + const firstRelease = service.release(); + const secondRelease = service.release(); + expect(secondRelease).toBe(firstRelease); + + let error: unknown; + try { + await firstRelease; + } catch (caught) { + error = caught; + } + + expect(error).toBeInstanceOf(AggregateError); + expect((error as AggregateError).errors).toEqual([ + subscriberFailure, + poolFailure, + ]); + expect(events).toEqual(['subscriber', 'pool']); + }); + + it('rethrows one owned service failure unchanged after attempting the pool', async () => { + const subscriberFailure = new Error('subscriber release failed'); + const events: string[] = []; + const service = makePgService({ + connectionString: 'postgres://unused/release-contract-single', + schemas: ['public'], + }); + const subscriber = service.pgSubscriber as unknown as { + release: () => Promise; + }; + const pool = service.adaptorSettings?.pool as unknown as { + end: () => Promise; + }; + subscriber.release = jest.fn(async () => { + events.push('subscriber'); + throw subscriberFailure; + }); + pool.end = jest.fn(async () => { + events.push('pool'); + }); + + await expect(service.release()).rejects.toBe(subscriberFailure); + expect(events).toEqual(['subscriber', 'pool']); + }); + + it('unlistens and returns the same real backend before service release resolves', async () => { + const fixture = await getConnections({}, []); + const poolConfig = { ...fixture.pg.config, max: 1 }; + const pool = fixture.manager.getPool(poolConfig); + const producer = fixture.pg; + const service = makePgService({ pool, schemas: ['public'] }); + try { + const iterator = await service.pgSubscriber!.subscribe('graphile_cache_release_test'); + let received = false; + const notification = iterator.next().then(result => { + expect(result.value).toBe('ready'); + received = true; + }); + // LISTEN startup is asynchronous; send until the real subscriber confirms + // readiness instead of assuming a timer implies the query has completed. + for (let attempt = 0; attempt < 100 && !received; attempt++) { + await producer.query("SELECT pg_notify('graphile_cache_release_test', 'ready')"); + await new Promise(resolve => setTimeout(resolve, 10)); + } + expect(received).toBe(true); + await notification; + expect(pool.totalCount).toBe(1); + expect(pool.idleCount).toBe(0); + await service.release(); + expect(pool.idleCount).toBe(1); + const channels = await pool.query('SELECT pg_listening_channels()'); + expect(channels.rows).toEqual([]); + expect(pool.totalCount).toBe(1); + } finally { + try { await service.release(); } + finally { await fixture.teardown(); } + } + }); +}); diff --git a/graphile/graphile-cache/src/__tests__/preset-services.test.ts b/graphile/graphile-cache/src/__tests__/preset-services.test.ts new file mode 100644 index 0000000000..f4b38a855b --- /dev/null +++ b/graphile/graphile-cache/src/__tests__/preset-services.test.ts @@ -0,0 +1,50 @@ +import { createPresetServicesReleaser } from '../preset-services'; + +describe('preset service ownership', () => { + it('releases unique services in reverse order exactly once', async () => { + const events: string[] = []; + const first = { + release: jest.fn(async () => { + events.push('first'); + }), + }; + const second = { + release: jest.fn(async () => { + events.push('second'); + }), + }; + const release = createPresetServicesReleaser({ + pgServices: [first, second, first], + }); + + const releases = [release(), release(), release()]; + expect(releases[0]).toBe(releases[1]); + expect(releases[1]).toBe(releases[2]); + await Promise.all(releases); + + expect(events).toEqual(['second', 'first']); + expect(first.release).toHaveBeenCalledTimes(1); + expect(second.release).toHaveBeenCalledTimes(1); + }); + + it('continues releasing services and preserves the first error', async () => { + const firstFailure = new Error('second failed'); + const first = { release: jest.fn().mockResolvedValue(undefined) }; + const second = { release: jest.fn().mockRejectedValue(firstFailure) }; + const release = createPresetServicesReleaser({ + pgServices: [first, second], + }); + + await expect(release()).rejects.toBe(firstFailure); + expect(first.release).toHaveBeenCalledTimes(1); + expect(second.release).toHaveBeenCalledTimes(1); + }); + + it('is a safe idempotent no-op when the preset has no services', async () => { + const release = createPresetServicesReleaser({}); + + const first = release(); + expect(release()).toBe(first); + await first; + }); +}); diff --git a/graphile/graphile-cache/src/__tests__/runtime-entry-usage.test.ts b/graphile/graphile-cache/src/__tests__/runtime-entry-usage.test.ts new file mode 100644 index 0000000000..f6a9408f77 --- /dev/null +++ b/graphile/graphile-cache/src/__tests__/runtime-entry-usage.test.ts @@ -0,0 +1,252 @@ +import { EventEmitter } from 'node:events'; + +import type { Request, Response } from 'express'; + +import { + retireGraphileEntry, + withGraphileEntryUsage, +} from '../runtime-entry-usage'; + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: Error): void; +} + +const deferred = (): Deferred => { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +}; + +interface FakeRequest extends EventEmitter { + aborted: boolean; + destroyed: boolean; + socket: { destroyed: boolean }; + destroy: jest.Mock; +} + +interface FakeResponse extends EventEmitter { + destroyed: boolean; + writableEnded: boolean; + destroy: jest.Mock; +} + +const makeRequest = (): Request => { + const req = new EventEmitter() as FakeRequest; + req.aborted = false; + req.destroyed = false; + req.socket = { destroyed: false }; + req.destroy = jest.fn(); + return req as unknown as Request; +}; + +const makeResponse = (): Response => { + const res = new EventEmitter() as FakeResponse; + res.destroyed = false; + res.writableEnded = false; + res.destroy = jest.fn(); + return res as unknown as Response; +}; + +const finish = (res: Response): void => { + (res as unknown as FakeResponse).writableEnded = true; + res.emit('finish'); +}; + +const close = (res: Response): void => { + res.emit('close'); +}; + +const abort = (req: Request): void => { + (req as unknown as FakeRequest).aborted = true; + req.emit('aborted'); +}; + +describe('Graphile cache entry request usage', () => { + it('holds an aborted request until its deferred handler settles', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + const work = deferred(); + const handler = jest.fn(() => work.promise); + + const request = withGraphileEntryUsage(entry, req, res, handler); + expect(handler).toHaveBeenCalledWith(req, res); + + const retiring = retireGraphileEntry(entry); + let drained = false; + void retiring.then(() => { + drained = true; + }); + abort(req); + expect((req as unknown as FakeRequest).destroy).toHaveBeenCalledTimes(1); + expect((res as unknown as FakeResponse).destroy).toHaveBeenCalledTimes(1); + await Promise.resolve(); + expect(drained).toBe(false); + + work.resolve(undefined); + await request; + await retiring; + expect(drained).toBe(true); + }); + + it('destroys once for an abnormal response close and tolerates repeated signals', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + const handler = jest.fn(async (): Promise => undefined); + + const request = withGraphileEntryUsage(entry, req, res, handler); + close(res); + close(res); + abort(req); + await request; + await retireGraphileEntry(entry); + + expect((req as unknown as FakeRequest).destroy).toHaveBeenCalledTimes(1); + expect((res as unknown as FakeResponse).destroy).toHaveBeenCalledTimes(1); + }); + + it('keeps the use while finish precedes handler settlement without destroying', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + const work = deferred(); + const request = withGraphileEntryUsage(entry, req, res, () => work.promise); + + finish(res); + const retiring = retireGraphileEntry(entry); + let drained = false; + void retiring.then(() => { + drained = true; + }); + await Promise.resolve(); + expect(drained).toBe(false); + expect((req as unknown as FakeRequest).destroy).not.toHaveBeenCalled(); + expect((res as unknown as FakeResponse).destroy).not.toHaveBeenCalled(); + + work.resolve(undefined); + await request; + await retiring; + }); + + it('keeps the use after handler settlement until response finish', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + const request = withGraphileEntryUsage( + entry, + req, + res, + async (): Promise => undefined + ); + + await request; + const retiring = retireGraphileEntry(entry); + let drained = false; + void retiring.then(() => { + drained = true; + }); + await Promise.resolve(); + expect(drained).toBe(false); + + finish(res); + await retiring; + }); + + it('counts concurrent users and resolves the shared last-use drain', async () => { + const entry = {}; + const req1 = makeRequest(); + const res1 = makeResponse(); + const req2 = makeRequest(); + const res2 = makeResponse(); + const work1 = deferred(); + const work2 = deferred(); + const first = withGraphileEntryUsage(entry, req1, res1, () => work1.promise); + const second = withGraphileEntryUsage(entry, req2, res2, () => work2.promise); + const retiring = retireGraphileEntry(entry); + + work1.resolve(undefined); + await first; + finish(res1); + let drained = false; + void retiring.then(() => { + drained = true; + }); + await Promise.resolve(); + expect(drained).toBe(false); + + work2.resolve(undefined); + await second; + finish(res2); + await retiring; + }); + + it('shares repeated retirement calls and rejects late entrants', async () => { + const entry = {}; + const first = retireGraphileEntry(entry); + const second = retireGraphileEntry(entry); + expect(second).toBe(first); + + await expect( + withGraphileEntryUsage(entry, makeRequest(), makeResponse(), async () => { + throw new Error('must not run'); + }) + ).rejects.toThrow('retiring'); + }); + + it('skips a request that has already ended without retaining the entry', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + (req as unknown as FakeRequest).aborted = true; + const handler = jest.fn(async (): Promise => undefined); + + await withGraphileEntryUsage(entry, req, res, handler); + expect(handler).not.toHaveBeenCalled(); + await retireGraphileEntry(entry); + }); + + it('does not treat an auto-destroyed request as ended while its socket is live', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + (req as unknown as FakeRequest).destroyed = true; + const handler = jest.fn(async (): Promise => undefined); + + await withGraphileEntryUsage(entry, req, res, handler); + expect(handler).toHaveBeenCalledWith(req, res); + + const retiring = retireGraphileEntry(entry); + finish(res); + await retiring; + }); + + it('propagates the exact handler error and releases after response end', async () => { + const entry = {}; + const req = makeRequest(); + const res = makeResponse(); + const failure = new Error('handler failed'); + + const request = withGraphileEntryUsage(entry, req, res, async () => { + throw failure; + }); + await expect(request).rejects.toBe(failure); + + const retiring = retireGraphileEntry(entry); + let drained = false; + void retiring.then(() => { + drained = true; + }); + await Promise.resolve(); + expect(drained).toBe(false); + + finish(res); + await retiring; + }); +}); diff --git a/graphile/graphile-cache/src/build-readiness.ts b/graphile/graphile-cache/src/build-readiness.ts new file mode 100644 index 0000000000..f51d63ed46 --- /dev/null +++ b/graphile/graphile-cache/src/build-readiness.ts @@ -0,0 +1,35 @@ +export interface GraphileBuildReadiness { + schemaResult: PromiseLike | unknown; + addTo(): PromiseLike | unknown; + ready(): PromiseLike | unknown; + release(): PromiseLike | unknown; + onReleaseError?(error: unknown): void; +} + +/** + * Resolve only after schema gathering and the HTTP adapter are ready. A failed + * generation reaches its release terminal state before the failure escapes. + */ +export const awaitGraphileBuildReadiness = async ( + build: GraphileBuildReadiness +): Promise => { + const schemaOutcome = Promise.resolve(build.schemaResult).then( + () => ({ ready: true as const }), + (error: unknown) => ({ ready: false as const, error }) + ); + try { + await build.addTo(); + const [schema] = await Promise.all([schemaOutcome, build.ready()]); + if ('error' in schema) throw schema.error; + } catch (error) { + // A failed adapter/readiness operation can race schema construction. Wait + // for schema work to settle before releasing the services it may still use. + await schemaOutcome; + try { + await build.release(); + } catch (releaseError) { + build.onReleaseError?.(releaseError); + } + throw error; + } +}; diff --git a/graphile/graphile-cache/src/create-instance.ts b/graphile/graphile-cache/src/create-instance.ts index 575b767589..7c58433421 100644 --- a/graphile/graphile-cache/src/create-instance.ts +++ b/graphile/graphile-cache/src/create-instance.ts @@ -2,16 +2,26 @@ import { createServer } from 'node:http'; import { Logger } from '@pgpmjs/logger'; import express from 'express'; +import type { NextFunction, Request, Response } from 'express'; import { grafserv } from 'grafserv/express/v4'; import { postgraphile } from 'postgraphile'; -import type { GraphileCacheEntry } from './graphile-cache'; +import { awaitGraphileBuildReadiness } from './build-readiness'; +import { + trackGraphileBuild, + type GraphileCacheEntry, +} from './graphile-cache'; +import { createPresetServicesReleaser } from './preset-services'; +import { withGraphileEntryUsage } from './runtime-entry-usage'; const log = new Logger('graphile-cache:create'); interface GraphileInstanceOptions { preset: any; cacheKey: string; + runtimePoolIdentity?: string; + logicalServiceKey?: string; + releaseRuntimePool?: () => void | Promise; /** * When true, a RealtimeManager is created and started alongside the * PostGraphile instance. The pool is extracted from the preset's @@ -36,28 +46,96 @@ interface GraphileInstanceOptions { * pool are extracted from the resolved preset's pgServices — no separate * pool parameter is needed. */ -export const createGraphileInstance = async ( +const createGraphileInstanceInternal = async ( opts: GraphileInstanceOptions ): Promise => { - const { preset, cacheKey, enableRealtime = false } = opts; + const { + preset, + cacheKey, + enableRealtime = false, + runtimePoolIdentity, + logicalServiceKey, + releaseRuntimePool, + } = opts; const pgl = postgraphile(preset); + const resolvedPreset = pgl.getResolvedPreset(); + const releasePresetServices = createPresetServicesReleaser(resolvedPreset); const serv = pgl.createServ(grafserv); - const handler = express(); - const httpServer = createServer(handler); - await serv.addTo(handler, httpServer); - await serv.ready(); + const app = express(); + const httpServer = createServer(app); + const directHandler = serv.createHandler() as unknown as ( + req: Request, + res: Response, + next?: NextFunction + ) => unknown; + let failedBuildReleasePromise: Promise | null = null; + const releaseFailedBuild = (): Promise => { + if (failedBuildReleasePromise) return failedBuildReleasePromise; + failedBuildReleasePromise = (async () => { + let firstError: unknown; + let failed = false; + try { + await pgl.release(); + } catch (error) { + firstError = error; + failed = true; + } + try { + await releasePresetServices(); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + try { + await releaseRuntimePool?.(); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + if (failed) throw firstError; + })(); + return failedBuildReleasePromise; + }; - const entry: GraphileCacheEntry = { + let entry!: GraphileCacheEntry; + entry = { pgl, serv, - handler, + handler: app, httpServer, cacheKey, createdAt: Date.now(), + releasePresetServices, + runtimePoolIdentity, + logicalServiceKey, + releaseRuntimePool, }; + app.use((req, res, next) => { + void withGraphileEntryUsage( + entry, + req, + res, + (request, response): Promise => + Promise.resolve(directHandler(request, response, next)) + ).catch(next); + }); + + await awaitGraphileBuildReadiness({ + schemaResult: pgl.getSchemaResult(), + addTo: () => serv.addTo(app, httpServer), + ready: () => serv.ready(), + release: releaseFailedBuild, + onReleaseError: (releaseError) => { + log.error( + `Failed to release PostGraphile[${cacheKey}] after build failure:`, + releaseError + ); + } + }); + if (enableRealtime) { try { const { RealtimeManager } = await import('graphile-realtime-subscriptions'); @@ -65,7 +143,6 @@ export const createGraphileInstance = async ( // Extract PgSubscriber and pool from the resolved preset's pgServices. // The pool is the same instance managed by pg-cache (via getPgPool) // and threaded into the preset by makePgService({ pool, schemas }). - const resolvedPreset = pgl.getResolvedPreset(); const pgService = (resolvedPreset as any).pgServices?.[0]; const pgSubscriber = pgService?.pgSubscriber ?? null; const pool = pgService?.adaptorSettings?.pool ?? null; @@ -93,3 +170,8 @@ export const createGraphileInstance = async ( return entry; }; + +export const createGraphileInstance = ( + opts: GraphileInstanceOptions +): Promise => + trackGraphileBuild(() => createGraphileInstanceInternal(opts)); diff --git a/graphile/graphile-cache/src/graphile-cache.ts b/graphile/graphile-cache/src/graphile-cache.ts index 83782c6a21..08535b48d1 100644 --- a/graphile/graphile-cache/src/graphile-cache.ts +++ b/graphile/graphile-cache/src/graphile-cache.ts @@ -8,6 +8,8 @@ import { LRUCache } from 'lru-cache'; import { pgCache } from 'pg-cache'; import type { PostGraphileInstance } from 'postgraphile'; +import { retireGraphileEntry } from './runtime-entry-usage'; + const log = new Logger('graphile-cache'); // --- Time Constants --- @@ -86,57 +88,177 @@ export interface GraphileCacheEntry { httpServer: HttpServer; cacheKey: string; createdAt: number; + /** Idempotent release for pgServices owned by this exact preset generation. */ + releasePresetServices?: () => Promise; + /** Opaque runtime pool identity retained by this exact Graphile generation. */ + runtimePoolIdentity?: string; + /** Logical service key used for matching entries during targeted flushes. */ + logicalServiceKey?: string; + /** Release the cache owner's runtime pool lease after preset services release. */ + releaseRuntimePool?: () => void | Promise; /** Optional RealtimeManager for cursor-tracked subscription delivery */ realtimeManager?: { stop(): Promise } | null; } -// Track disposed entries to prevent double-disposal -const disposedKeys = new Set(); +const disposalPromises = new WeakMap>(); +const activeDisposals = new Set>(); +const activeBuilds = new Set>(); +let graphileCacheClosing = false; +let graphileCacheEpoch = 0; // Track keys that are being manually evicted for accurate eviction reason const manualEvictionKeys = new Set(); +const GRAPHILE_CACHE_CLOSING = 'Graphile cache is closing'; + +/** Throw synchronously when a new Graphile build cannot be admitted. */ +export const assertGraphileCacheOpen = (): void => { + if (graphileCacheClosing) { + throw new Error(GRAPHILE_CACHE_CLOSING); + } +}; + +/** + * Admit one complete Graphile build and retain it through settlement. A build + * that completes after shutdown begins is disposed rather than being + * published into the cache. + */ +export function trackGraphileBuild( + factory: () => Promise +): Promise { + assertGraphileCacheOpen(); + const buildEpoch = graphileCacheEpoch; + + let resolveTracked!: (entry: GraphileCacheEntry) => void; + let rejectTracked!: (error: unknown) => void; + const tracked = new Promise((resolve, reject) => { + resolveTracked = resolve; + rejectTracked = reject; + }); + activeBuilds.add(tracked); + void tracked.then( + () => activeBuilds.delete(tracked), + () => activeBuilds.delete(tracked) + ); + + let factoryPromise: Promise; + try { + factoryPromise = Promise.resolve(factory()); + } catch (error) { + rejectTracked(error); + return tracked; + } + + void factoryPromise + .then(async (entry) => { + if (graphileCacheClosing || buildEpoch !== graphileCacheEpoch) { + await disposeUncachedEntry(entry); + throw new Error(GRAPHILE_CACHE_CLOSING); + } + return entry; + }) + .then(resolveTracked, rejectTracked); + return tracked; +} + +const waitForActiveBuilds = async (): Promise => { + while (activeBuilds.size > 0) { + await Promise.allSettled([...activeBuilds]); + } +}; + /** * Dispose a PostGraphile v5 cache entry * * Properly releases resources by: * 1. Closing the HTTP server if listening - * 2. Releasing the PostGraphile instance (which internally releases grafserv) - * - * Uses disposedKeys set to prevent double-disposal when closeAllCaches() - * explicitly disposes entries and then clear() triggers the dispose callback. + * 2. Stopping the realtime manager + * 3. Releasing PostGraphile/Grafserv and preset services */ -const disposeEntry = async (entry: GraphileCacheEntry, key: string): Promise => { - // Prevent double-disposal - if (disposedKeys.has(key)) { - return; - } - disposedKeys.add(key); - +const releaseEntry = async ( + entry: GraphileCacheEntry, + key: string +): Promise => { log.debug(`Disposing PostGraphile[${key}]`); + let firstError: unknown; + let failed = false; + await retireGraphileEntry(entry); try { - // Close HTTP server if it's listening if (entry.httpServer?.listening) { await new Promise((resolve) => { entry.httpServer.close(() => resolve()); }); } - // Stop RealtimeManager if present (before releasing PostGraphile) + } catch (error) { + firstError = error; + failed = true; + } + try { if (entry.realtimeManager) { - try { - await entry.realtimeManager.stop(); - } catch (err) { - log.error(`Error stopping RealtimeManager for PostGraphile[${key}]:`, err); - } + await entry.realtimeManager.stop(); } - // Release PostGraphile instance (this also releases grafserv internally) - if (entry.pgl) { - await entry.pgl.release(); - } - } catch (err) { - log.error(`Error disposing PostGraphile[${key}]:`, err); - } finally { - disposedKeys.delete(key); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + try { + await entry.pgl.release(); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + try { + await entry.releasePresetServices?.(); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + try { + await entry.releaseRuntimePool?.(); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + if (failed) throw firstError; +}; + +/** + * Coalesce teardown by exact entry identity, not by its reusable cache key. + */ +const scheduleDisposal = ( + entry: GraphileCacheEntry, + key: string +): Promise => { + const existing = disposalPromises.get(entry); + if (existing) return existing; + + const pending = releaseEntry(entry, key); + + disposalPromises.set(entry, pending); + activeDisposals.add(pending); + void pending + .catch((error) => { + log.error(`Failed to dispose PostGraphile[${key}]:`, error); + }) + .finally(() => activeDisposals.delete(pending)); + return pending; +}; + +/** Dispose a generation that was built but never published in the cache. */ +export const disposeUncachedEntry = ( + entry: GraphileCacheEntry, + key = entry.cacheKey +): Promise => scheduleDisposal(entry, key); + +/** Await the terminal result for an entry whose disposal has been scheduled. */ +export const waitForEntryDisposal = ( + entry: GraphileCacheEntry +): Promise => disposalPromises.get(entry) ?? Promise.resolve(); + +/** Await every disposal that is active at or begins during this drain. */ +export const waitForActiveDisposals = async (): Promise => { + while (activeDisposals.size > 0) { + await Promise.allSettled([...activeDisposals]); } }; @@ -176,11 +298,7 @@ export const graphileCache = new LRUCache({ log.debug(`Evicting PostGraphile[${key}] (reason: ${reason})`); - // LRU dispose is synchronous, but v5 disposal is async - // Fire and forget the async cleanup - disposeEntry(entry, key).catch((err) => { - log.error(`Failed to dispose PostGraphile[${key}]:`, err); - }); + scheduleDisposal(entry, key); } }); @@ -216,7 +334,10 @@ export function clearMatchingEntries(pattern: RegExp): number { let cleared = 0; for (const key of graphileCache.keys()) { - if (pattern.test(key)) { + const entry = graphileCache.peek(key); + const matchKey = entry?.logicalServiceKey ?? key; + pattern.lastIndex = 0; + if (pattern.test(matchKey)) { // Mark as manual eviction before deleting manualEvictionKeys.add(key); graphileCache.delete(key); @@ -234,7 +355,11 @@ const unregister = pgCache.registerCleanupCallback((pgPoolKey: string) => { // Remove graphile entries that reference this pool key graphileCache.forEach((entry, k) => { - if (entry.cacheKey.includes(pgPoolKey)) { + if ( + entry.runtimePoolIdentity === undefined || entry.runtimePoolIdentity === null + ? entry.cacheKey.includes(pgPoolKey) + : entry.runtimePoolIdentity === pgPoolKey + ) { log.debug(`Removing graphileCache[${k}] due to pgPool[${pgPoolKey}] disposal`); manualEvictionKeys.add(k); graphileCache.delete(k); @@ -245,6 +370,16 @@ const unregister = pgCache.registerCleanupCallback((pgPoolKey: string) => { // Enhanced close function that handles all caches const closePromise: { promise: Promise | null } = { promise: null }; +/** Clear all resident entries and await every exact-generation disposal. */ +export const clearGraphileCache = async (): Promise => { + for (const key of graphileCache.keys()) { + manualEvictionKeys.add(key); + } + graphileCache.clear(); + await waitForActiveDisposals(); + manualEvictionKeys.clear(); +}; + /** * Close all caches and release resources * @@ -259,37 +394,21 @@ const closePromise: { promise: Promise | null } = { promise: null }; export const closeAllCaches = async (verbose = false): Promise => { if (closePromise.promise) return closePromise.promise; + graphileCacheClosing = true; + graphileCacheEpoch += 1; closePromise.promise = (async () => { try { if (verbose) log.info('Closing all server caches...'); - // Collect all entries and dispose them properly - const entries = [...graphileCache.entries()]; - - // Mark all as manual evictions - for (const [key] of entries) { - manualEvictionKeys.add(key); - } - - const disposePromises = entries.map(([key, entry]) => - disposeEntry(entry, key) - ); - - // Wait for all disposals to complete - await Promise.allSettled(disposePromises); - - // Clear the cache after disposal (dispose callback will no-op due to disposedKeys) - graphileCache.clear(); - - // Clear disposed keys tracking after full cleanup - disposedKeys.clear(); - manualEvictionKeys.clear(); + await waitForActiveBuilds(); + await clearGraphileCache(); // Close pg pools await pgCache.close(); if (verbose) log.success('All caches disposed.'); } finally { + graphileCacheClosing = false; closePromise.promise = null; } })(); diff --git a/graphile/graphile-cache/src/index.ts b/graphile/graphile-cache/src/index.ts index 9a845fafe3..7d45edeab2 100644 --- a/graphile/graphile-cache/src/index.ts +++ b/graphile/graphile-cache/src/index.ts @@ -8,9 +8,11 @@ export { CacheEvictionEvent, // Cache stats CacheStats, + clearGraphileCache, // Clear matching entries clearMatchingEntries, closeAllCaches, + disposeUncachedEntry, // Eviction tracking EvictionReason, FIVE_MINUTES_MS, @@ -19,11 +21,21 @@ export { // Cache instance and entry type graphileCache, GraphileCacheEntry, + assertGraphileCacheOpen, + trackGraphileBuild, // Time constants - ONE_HOUR_MS} from './graphile-cache'; + ONE_HOUR_MS, + waitForActiveDisposals, + waitForEntryDisposal} from './graphile-cache'; // Factory for creating PostGraphile v5 instances export { createGraphileInstance } from './create-instance'; +export type { GraphilePublicRequestHandler } from './runtime-entry-usage'; +export { + retireGraphileEntry, + withGraphileEntryUsage, +} from './runtime-entry-usage'; + // Generic module config cache for plugin lookups export { ModuleConfigCache, ModuleConfigCacheOptions } from './module-config-cache'; diff --git a/graphile/graphile-cache/src/preset-services.ts b/graphile/graphile-cache/src/preset-services.ts new file mode 100644 index 0000000000..9c76c16ed7 --- /dev/null +++ b/graphile/graphile-cache/src/preset-services.ts @@ -0,0 +1,29 @@ +interface ReleasablePresetService { + release?: () => void | PromiseLike; +} + +/** Own and release the unique pgServices for one resolved preset generation. */ +export const createPresetServicesReleaser = (resolvedPreset: { + pgServices?: readonly ReleasablePresetService[]; +}): (() => Promise) => { + const services = [...new Set(resolvedPreset.pgServices ?? [])]; + let releasePromise: Promise | null = null; + + return (): Promise => { + if (releasePromise) return releasePromise; + releasePromise = (async () => { + let firstError: unknown; + let failed = false; + for (const service of [...services].reverse()) { + try { + await service.release?.(); + } catch (error) { + if (!failed) firstError = error; + failed = true; + } + } + if (failed) throw firstError; + })(); + return releasePromise; + }; +}; diff --git a/graphile/graphile-cache/src/runtime-entry-usage.ts b/graphile/graphile-cache/src/runtime-entry-usage.ts new file mode 100644 index 0000000000..cff279ddd5 --- /dev/null +++ b/graphile/graphile-cache/src/runtime-entry-usage.ts @@ -0,0 +1,195 @@ +import type { Request, Response } from 'express'; + +/** + * The direct request handler returned by Grafserv's public `createHandler()`. + * + * Grafserv's Node declaration historically says `void` because the function is + * also suitable for an HTTP server callback, but its implementation is async + * and returns the request promise. This contract deliberately accepts that + * direct handler rather than an Express application. + */ +export type GraphilePublicRequestHandler = ( + req: Request, + res: Response +) => Promise; + +interface EntryUsageState { + activeUses: number; + retiring: boolean; + drain: Promise | null; + resolveDrain: (() => void) | null; +} + +interface Destroyable { + destroy?: () => unknown; +} + +/** + * Runtime state is keyed by entry object identity. A cache key is not enough: + * two generations may intentionally share one key while having different + * pools and Grafserv services. + */ +const entryUsage = new WeakMap(); + +const getEntryUsage = (entry: object): EntryUsageState => { + let state = entryUsage.get(entry); + if (!state) { + state = { + activeUses: 0, + retiring: false, + drain: null, + resolveDrain: null, + }; + entryUsage.set(entry, state); + } + return state; +}; + +const settleDrainIfIdle = (state: EntryUsageState): void => { + if (state.retiring && state.activeUses === 0 && state.resolveDrain) { + const resolveDrain = state.resolveDrain; + state.resolveDrain = null; + resolveDrain(); + } +}; + +const getRetirementDrain = (state: EntryUsageState): Promise => { + if (!state.drain) { + state.drain = new Promise((resolve) => { + state.resolveDrain = resolve; + }); + } + settleDrainIfIdle(state); + return state.drain; +}; + +const isAlreadyEnded = (req: Request, res: Response): boolean => + Boolean( + req.aborted || + req.socket?.destroyed || + res.destroyed || + res.writableEnded + ); + +const destroyOnce = ( + req: Request, + res: Response, + destroyed: { value: boolean } +): void => { + if (destroyed.value) return; + destroyed.value = true; + + // These are the native Node request/response destroy methods. Calling each + // at most once is important because abort and close can race. + const requestDestroy = (req as unknown as Destroyable).destroy; + if (typeof requestDestroy === 'function') { + requestDestroy.call(req); + } + const responseDestroy = (res as unknown as Destroyable).destroy; + if (typeof responseDestroy === 'function') { + responseDestroy.call(res); + } +}; + +const removeListener = ( + target: Request | Response, + event: string, + listener: () => void +): void => { + target.removeListener(event, listener); +}; + +/** + * Tracks one direct Grafserv request against one exact cache entry. + * + * A use is retained until both independent conditions hold: + * - the Grafserv handler's promise has settled; and + * - the response has finished, closed, or the request has aborted. + * + * The returned promise follows the handler. In particular, handler errors are + * rethrown as the same value immediately so the caller's error handling can + * complete the response while the entry remains protected by the latch. + */ +export async function withGraphileEntryUsage( + entry: object, + req: Request, + res: Response, + handler: GraphilePublicRequestHandler +): Promise { + // Do this before acquiring state: a request which is already over must not + // keep a retiring entry alive, and its handler must never be invoked. + if (isAlreadyEnded(req, res)) return; + + const state = getEntryUsage(entry); + if (state.retiring) { + throw new Error('Graphile cache entry is retiring; new requests are denied'); + } + + state.activeUses += 1; + + let handlerSettled = false; + let terminal = false; + let responseFinished = false; + let released = false; + const destroyed = { value: false }; + + let onFinish: () => void; + let onClose: () => void; + let onAbort: () => void; + + const releaseIfReady = (): void => { + if (!handlerSettled || !terminal || released) return; + released = true; + removeListener(res, 'finish', onFinish); + removeListener(res, 'close', onClose); + removeListener(req, 'aborted', onAbort); + state.activeUses -= 1; + settleDrainIfIdle(state); + }; + + onFinish = () => { + if (terminal) return; + responseFinished = true; + terminal = true; + releaseIfReady(); + }; + + onClose = () => { + if (terminal) return; + terminal = true; + if (!responseFinished) { + destroyOnce(req, res, destroyed); + } + releaseIfReady(); + }; + + onAbort = () => { + if (terminal) return; + terminal = true; + destroyOnce(req, res, destroyed); + releaseIfReady(); + }; + + res.once('finish', onFinish); + res.once('close', onClose); + req.once('aborted', onAbort); + + try { + await handler(req, res); + } finally { + handlerSettled = true; + releaseIfReady(); + } +} + +/** + * Marks an exact cache entry as retiring synchronously, then waits for every + * request whose two-part latch is still open. Repeated calls share one drain. + */ +export function retireGraphileEntry(entry: object): Promise { + const state = getEntryUsage(entry); + if (!state.retiring) { + state.retiring = true; + } + return getRetirementDrain(state); +} diff --git a/graphql/env/src/__tests__/runtime-pg.test.ts b/graphql/env/src/__tests__/runtime-pg.test.ts new file mode 100644 index 0000000000..6c738f5976 --- /dev/null +++ b/graphql/env/src/__tests__/runtime-pg.test.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { getGraphQLEnvVars } from '../env'; +import { getEnvOptions } from '../merge'; + +describe('GraphQL runtime PostgreSQL environment', () => { + it('maps dedicated runtime credentials without changing control-plane pg', () => { + const result = getGraphQLEnvVars({ + GRAPHQL_RUNTIME_PGUSER: 'graphql_runtime', + GRAPHQL_RUNTIME_PGPASSWORD: 'runtime-secret', + }); + + expect(result.runtimePg).toEqual({ + user: 'graphql_runtime', + password: 'runtime-secret', + }); + expect(result.pg).toBeUndefined(); + }); + + it('does not create a runtime override when both variables are absent', () => { + expect(getGraphQLEnvVars({}).runtimePg).toBeUndefined(); + }); + + it('merges static runtime config, env password, and exact route identity', () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'graphql-runtime-pg-')); + const identity = { + databaseId: 'database-a', + databaseName: 'tenant_a', + apiId: 'api-a', + schemas: ['tenant_a_public'], + roles: ['anonymous', 'authenticated'], + }; + fs.writeFileSync( + path.join(cwd, 'pgpm.json'), + JSON.stringify({ + runtimePg: { + host: 'runtime.internal', + database: 'tenant_a', + user: 'tenant_runtime', + password: 'config-secret', + }, + runtimePgStaticIdentity: identity, + }) + ); + + try { + const result = getEnvOptions({}, cwd, { + GRAPHQL_RUNTIME_PGPASSWORD: 'env-secret', + }); + expect(result.runtimePg).toMatchObject({ + host: 'runtime.internal', + database: 'tenant_a', + user: 'tenant_runtime', + password: 'env-secret', + }); + expect(result.runtimePgStaticIdentity).toEqual(identity); + } finally { + fs.rmSync(cwd, { recursive: true, force: true }); + } + }); +}); diff --git a/graphql/env/src/env.ts b/graphql/env/src/env.ts index f11ffb5af8..d878b8f783 100644 --- a/graphql/env/src/env.ts +++ b/graphql/env/src/env.ts @@ -8,6 +8,9 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial const { GRAPHILE_SCHEMA, + GRAPHQL_RUNTIME_PGUSER, + GRAPHQL_RUNTIME_PGPASSWORD, + FEATURES_SIMPLE_INFLECTION, FEATURES_OPPOSITE_BASE_NAMES, FEATURES_POSTGIS, @@ -49,6 +52,12 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial ); return { + ...((GRAPHQL_RUNTIME_PGUSER || GRAPHQL_RUNTIME_PGPASSWORD) && { + runtimePg: { + ...(GRAPHQL_RUNTIME_PGUSER && { user: GRAPHQL_RUNTIME_PGUSER }), + ...(GRAPHQL_RUNTIME_PGPASSWORD && { password: GRAPHQL_RUNTIME_PGPASSWORD }) + } + }), graphile: { ...(GRAPHILE_SCHEMA && { schema: GRAPHILE_SCHEMA.includes(',') diff --git a/graphql/env/src/merge.ts b/graphql/env/src/merge.ts index 15f1402c53..977c6c57f3 100644 --- a/graphql/env/src/merge.ts +++ b/graphql/env/src/merge.ts @@ -44,6 +44,10 @@ export const getEnvOptions = ( ...(configOptions.graphile && { graphile: configOptions.graphile }), ...(configOptions.features && { features: configOptions.features }), ...(configOptions.api && { api: configOptions.api }), + ...(configOptions.runtimePg && { runtimePg: configOptions.runtimePg }), + ...(configOptions.runtimePgStaticIdentity && { + runtimePgStaticIdentity: configOptions.runtimePgStaticIdentity + }), ...(configOptions.sms && { sms: configOptions.sms }), }, graphqlEnvOptions, diff --git a/graphql/server-test/__tests__/runtime-pg.integration.test.ts b/graphql/server-test/__tests__/runtime-pg.integration.test.ts new file mode 100644 index 0000000000..5eb56c121b --- /dev/null +++ b/graphql/server-test/__tests__/runtime-pg.integration.test.ts @@ -0,0 +1,263 @@ +/** + * Runtime PostgreSQL credentials through the production HTTP/routing path. + * + * This test deliberately observes only GraphQL responses. The session_user + * function is SQL INVOKER, so the returned login proves which runtime pool + * executed the request while routing still uses the control-plane connection and Graphile + * introspection assumes the explicitly configured anonymous role. + */ +import { randomUUID } from 'crypto'; +import path from 'path'; +import type { + RuntimePgResolver, + RuntimePgResolverInput +} from '@constructive-io/graphql-types'; + +import { getConnections, seed } from '../src'; + +jest.setTimeout(60000); + +const sharedSeedRoot = path.join(__dirname, '..', '..', '..', '__fixtures__', 'seed'); +const shared = (...segments: string[]) => path.join(sharedSeedRoot, ...segments); +const pgpmWorkspace = path.join(sharedSeedRoot, '..', '..'); +const schemas = ['simple-pets-public', 'simple-pets-pets-public']; +const metaSchemas = [ + 'catalog_private', + 'routing_public', + 'apps_public', + 'metaschema_public', + 'metaschema_modules_public' +]; +const scopedDatabaseId = '80a2eaaf-f77e-4bfe-8506-df929ef1b8d9'; +const appApiId = '6c9997a4-591b-4cb3-9313-4ef45d6f134e'; +const anonymousRole = 'anonymous'; +const authenticatedRole = 'authenticated'; +const roleSuffix = `${process.pid}_${randomUUID().replace(/-/g, '')}`; +const runtimeRoleA = `runtime_http_a_${roleSuffix}`; +const runtimeRoleB = `runtime_http_b_${roleSuffix}`; +const runtimePasswordA = `runtime-http-a-${randomUUID()}`; +const runtimePasswordB = `runtime-http-b-${randomUUID()}`; + +const quoteIdentifier = (value: string): string => + `"${value.replace(/"/g, '""')}"`; +const quoteLiteral = (value: string): string => + `'${value.replace(/'/g, "''")}'`; + +const roleSetup = seed.fn(async ({ pg, config }) => { + await pg.query(` + CREATE ROLE ${quoteIdentifier(runtimeRoleA)} + LOGIN PASSWORD ${quoteLiteral(runtimePasswordA)} + NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; + CREATE ROLE ${quoteIdentifier(runtimeRoleB)} + LOGIN PASSWORD ${quoteLiteral(runtimePasswordB)} + NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS; + + GRANT ${quoteIdentifier(anonymousRole)}, ${quoteIdentifier(authenticatedRole)} + TO ${quoteIdentifier(runtimeRoleA)}, ${quoteIdentifier(runtimeRoleB)}; + GRANT CONNECT ON DATABASE ${quoteIdentifier(config.database)} + TO ${quoteIdentifier(runtimeRoleA)}, ${quoteIdentifier(runtimeRoleB)}; + + CREATE OR REPLACE FUNCTION "simple-pets-public".runtime_session_user() + RETURNS text + LANGUAGE sql + STABLE + SECURITY INVOKER + AS $$ SELECT session_user::text $$; + GRANT USAGE ON SCHEMA "simple-pets-public" TO ${quoteIdentifier(anonymousRole)}; + GRANT EXECUTE ON FUNCTION "simple-pets-public".runtime_session_user() + TO ${quoteIdentifier(anonymousRole)}; + `); +}); + +type ResolverMode = 'a' | 'b' | 'reject' | 'missing' | 'mismatch'; + +describe('runtime PostgreSQL credentials over scoped HTTP', () => { + let fixture: Awaited>; + let resolverMode: ResolverMode = 'a'; + let resolverCalls = 0; + const routeInputs: RuntimePgResolverInput[] = []; + + const runtimePgResolver: RuntimePgResolver = async (input) => { + resolverCalls += 1; + routeInputs.push({ + databaseId: input.databaseId, + databaseName: input.databaseName, + apiId: input.apiId, + schemas: [...input.schemas], + roles: [...input.roles] as [string, string] + }); + + if (resolverMode === 'reject') { + throw new Error('runtime resolver rejected'); + } + if (resolverMode === 'missing') { + return { database: input.databaseName, user: runtimeRoleA }; + } + + const credentials = resolverMode === 'b' + ? { user: runtimeRoleB, password: runtimePasswordB } + : { user: runtimeRoleA, password: runtimePasswordA }; + if (resolverMode === 'mismatch') { + return { + database: input.databaseName, + ...credentials, + host: '127.0.0.2', + port: 1 + }; + } + return { database: input.databaseName, ...credentials }; + }; + + const postSessionUser = () => + fixture.request + .post('/graphql') + .set('Host', 'app.test.constructive.io') + .send({ query: '{ runtimeSessionUser }' }); + + const expectRuntimeError = ( + response: { status: number; body: { data?: unknown; error?: { code?: string } } }, + code: string + ): void => { + expect(response.status).toBe(500); + expect(response.body.error?.code).toBe(code); + expect(response.body.data).toBeUndefined(); + }; + + beforeAll(async () => { + fixture = await getConnections( + { + schemas, + authRole: anonymousRole, + runtimePgResolver, + server: { + useRouting: true, + api: { + isPublic: true, + metaSchemas, + introspectionRole: anonymousRole + } + } + }, + [ + seed.pgpm(pgpmWorkspace), + seed.sqlfile([ + shared('app-schemas', 'simple-pets', 'schema.sql'), + shared('scoped', 'test-data.sql'), + shared('app-schemas', 'simple-pets', 'test-data.sql') + ]), + roleSetup + ] + ); + }); + + beforeEach(async () => { + await fixture.pg.beforeEach(); + await fixture.db.beforeEach(); + }); + + afterEach(async () => { + try { await fixture.db.afterEach(); } + finally { await fixture.pg.afterEach(); } + }); + + afterAll(async () => { + if (!fixture) return; + + let firstError: unknown; + const capture = (error: unknown): void => { + if (firstError === undefined) firstError = error; + }; + + // Stop Graphile and runtime pools before dropping the login roles. The + // root fixture client remains available for cleanup until db teardown. + try { + await fixture.server.stop(); + } catch (error) { + capture(error); + } + + try { + await fixture.pg.query(` + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE usename IN (${quoteLiteral(runtimeRoleA)}, ${quoteLiteral(runtimeRoleB)}) + AND pid <> pg_backend_pid(); + `); + await fixture.pg.query(` + DROP OWNED BY ${quoteIdentifier(runtimeRoleA)}, ${quoteIdentifier(runtimeRoleB)}; + DROP ROLE ${quoteIdentifier(runtimeRoleA)}, ${quoteIdentifier(runtimeRoleB)}; + `); + } catch (error) { + capture(error); + } + + try { + // server.stop() is idempotent; teardown still closes the database and + // all pgsql-test-owned clients/pools even when an earlier step failed. + await fixture.teardown(); + } catch (error) { + capture(error); + } + + if (firstError !== undefined) throw firstError; + }); + + it('uses each resolved login through Graphile and isolates cache generations', async () => { + resolverMode = 'a'; + const first = await postSessionUser(); + expect(first.status).toBe(200); + expect(first.body.errors).toBeUndefined(); + expect(first.body.data.runtimeSessionUser).toBe(runtimeRoleA); + expect(resolverCalls).toBe(1); + + const cacheHit = await postSessionUser(); + expect(cacheHit.status).toBe(200); + expect(cacheHit.body.errors).toBeUndefined(); + expect(cacheHit.body.data.runtimeSessionUser).toBe(runtimeRoleA); + expect(resolverCalls).toBe(2); + + resolverMode = 'b'; + const rotated = await postSessionUser(); + expect(rotated.status).toBe(200); + expect(rotated.body.errors).toBeUndefined(); + expect(rotated.body.data.runtimeSessionUser).toBe(runtimeRoleB); + expect(resolverCalls).toBe(3); + + expect(routeInputs).toHaveLength(3); + expect(routeInputs[0]).toEqual({ + databaseId: scopedDatabaseId, + databaseName: fixture.pg.config.database, + apiId: appApiId, + schemas, + roles: [anonymousRole, authenticatedRole] + }); + expect(routeInputs[1]).toEqual(routeInputs[0]); + expect(routeInputs[2]).toEqual(routeInputs[0]); + }); + + it('returns resolver and runtime-target errors without falling back', async () => { + const callsBeforeErrors = resolverCalls; + + resolverMode = 'reject'; + const rejected = await postSessionUser(); + expectRuntimeError(rejected, 'INTERNAL_ERROR'); + expect(resolverCalls).toBe(callsBeforeErrors + 1); + + resolverMode = 'missing'; + const missing = await postSessionUser(); + expectRuntimeError(missing, 'INTERNAL_ERROR'); + expect(resolverCalls).toBe(callsBeforeErrors + 2); + + resolverMode = 'mismatch'; + const mismatched = await postSessionUser(); + expectRuntimeError(mismatched, 'INTERNAL_ERROR'); + expect(resolverCalls).toBe(callsBeforeErrors + 3); + + resolverMode = 'b'; + const recovered = await postSessionUser(); + expect(recovered.status).toBe(200); + expect(recovered.body.errors).toBeUndefined(); + expect(recovered.body.data.runtimeSessionUser).toBe(runtimeRoleB); + expect(resolverCalls).toBe(callsBeforeErrors + 4); + }); +}); diff --git a/graphql/server-test/src/get-connections.ts b/graphql/server-test/src/get-connections.ts index 875c30b9cd..80dfe9c056 100644 --- a/graphql/server-test/src/get-connections.ts +++ b/graphql/server-test/src/get-connections.ts @@ -55,7 +55,14 @@ export const getConnections = async ( exposedSchemas: input.schemas, ...(input.authRole && { anonRole: input.authRole, roleName: input.authRole }) }, - graphile: input.graphile + graphile: input.graphile, + ...(input.runtimePg !== undefined && { runtimePg: input.runtimePg }), + ...(input.runtimePgStaticIdentity !== undefined && { + runtimePgStaticIdentity: input.runtimePgStaticIdentity + }), + ...(input.runtimePgResolver !== undefined && { + runtimePgResolver: input.runtimePgResolver + }) }); // Start the HTTP server. Suites default to the production scoped-routing diff --git a/graphql/server-test/src/types.ts b/graphql/server-test/src/types.ts index 2d1119be2e..a7c341132d 100644 --- a/graphql/server-test/src/types.ts +++ b/graphql/server-test/src/types.ts @@ -1,4 +1,10 @@ -import type { ApiOptions,GraphileOptions } from '@constructive-io/graphql-types'; +import type { + ApiOptions, + GraphileOptions, + RuntimePgConfig, + RuntimePgResolver, + RuntimePgResolverInput +} from '@constructive-io/graphql-types'; import type { DocumentNode, GraphQLError } from 'graphql'; import type { Server } from 'http'; import type { PgTestClient } from 'pgsql-test/test-client'; @@ -71,6 +77,12 @@ export interface GetConnectionsInput { authRole?: string; /** Graphile/PostGraphile configuration options */ graphile?: GraphileOptions; + /** Static least-privilege tenant execution login. */ + runtimePg?: RuntimePgConfig; + /** Exact route authorized to use `runtimePg`. */ + runtimePgStaticIdentity?: RuntimePgResolverInput; + /** Resolve one least-privilege login from credential-free route facts. */ + runtimePgResolver?: RuntimePgResolver; /** Server configuration options (port, host, and API configuration) */ server?: ServerOptions; } diff --git a/graphql/server/README.md b/graphql/server/README.md index 0717e31167..db31dca463 100644 --- a/graphql/server/README.md +++ b/graphql/server/README.md @@ -149,3 +149,15 @@ Use `supertest` or your HTTP client of choice against `/graphql`. For RLS-aware - `@constructive-io/graphql-types` - shared types and defaults - `graphile-settings` - PostGraphile configuration - `graphile-meta-schema` - meta schema support + +### Runtime PostgreSQL connection ownership + +The production server forwards `runtimePg`, `runtimePgStaticIdentity`, and `runtimePgResolver` to the request context. Configure either a static login bound to one exact route, or a resolver receiving frozen database/API/schema/role facts. A configured login must explicitly provide database, user, and password; its effective host, port, database, and driver target must match the control connection's routed database. Invalid configuration fails before acquisition. With neither option configured, the existing single-login deployment remains supported. + +Routing and module loaders retain control-plane pools. Graphile uses the already resolved context pool and canonical request settings. Introspection keeps the configured `api.introspectionRole`; the runtime login needs permission to assume that role and the served roles, plus any enabled module/realtime grants. This is credential separation, not the F18 runtime privilege audit. + +Each Graphile cache key combines the logical service key with an opaque process-local pool identity. Credential rotation creates a separate generation. The request context supplies an optional `retainRuntimePool()` capability; the server requires it and acquires a cache lease before asynchronous module discovery or schema work. It never reruns the resolver. Flush operations match logical service keys across all credential generations. + +Eviction retires an exact entry and waits for both the direct Grafserv handler promise and response termination before releasing its services and cache lease. Request abort alone does not imply a pending query has finished. Process cache shutdown closes build admission, drains in-flight builds, and disposes unpublished results. Long-lived responses must finish or be cancelled by the HTTP lifecycle before graceful cleanup can complete. + +The notification broker remains a separately tested capability; this change does not enable the F24 generation subscription consumer. diff --git a/graphql/server/src/middleware/__tests__/flush.test.ts b/graphql/server/src/middleware/__tests__/flush.test.ts index a5a44d1671..360edfd35d 100644 --- a/graphql/server/src/middleware/__tests__/flush.test.ts +++ b/graphql/server/src/middleware/__tests__/flush.test.ts @@ -6,11 +6,16 @@ import { graphileCache } from 'graphile-cache'; import { createFlushMiddleware } from '../flush'; const TOKEN = 'flush-secret-token'; +const SERVICE_KEY = 'tenant.example.com'; +const ROTATED_CACHE_KEYS = [ + `${SERVICE_KEY}:runtime:old`, + `${SERVICE_KEY}:runtime:new`, +]; const makeReq = (url: string, authorization?: string): Request => { const req: any = { url, - svc_key: 'tenant.example.com', + svc_key: SERVICE_KEY, get: (name: string) => name.toLowerCase() === 'authorization' ? authorization : undefined }; @@ -44,8 +49,9 @@ describe('createFlushMiddleware', () => { }); afterEach(() => { - graphileCache.delete('tenant.example.com'); - svcCache.delete('tenant.example.com'); + graphileCache.delete(SERVICE_KEY); + for (const key of ROTATED_CACHE_KEYS) graphileCache.delete(key); + svcCache.delete(SERVICE_KEY); }); it('passes non-flush requests through', async () => { @@ -63,8 +69,31 @@ describe('createFlushMiddleware', () => { next ); expect(res.statusCode).toBe(200); - expect(graphileCache.get('tenant.example.com')).toBeUndefined(); - expect(svcCache.get('tenant.example.com')).toBeUndefined(); + expect(graphileCache.get(SERVICE_KEY)).toBeUndefined(); + expect(svcCache.get(SERVICE_KEY)).toBeUndefined(); + }); + + it('flushes every physical generation for one logical service key', async () => { + graphileCache.set(ROTATED_CACHE_KEYS[0], { + cached: 'old-credentials', + logicalServiceKey: SERVICE_KEY, + } as any); + graphileCache.set(ROTATED_CACHE_KEYS[1], { + cached: 'new-credentials', + logicalServiceKey: SERVICE_KEY, + } as any); + + const res = makeRes(); + await createFlushMiddleware(opts(TOKEN))( + makeReq('/flush', `Bearer ${TOKEN}`), + res, + next + ); + + expect(res.statusCode).toBe(200); + expect(graphileCache.get(ROTATED_CACHE_KEYS[0])).toBeUndefined(); + expect(graphileCache.get(ROTATED_CACHE_KEYS[1])).toBeUndefined(); + expect(svcCache.get(SERVICE_KEY)).toBeUndefined(); }); it.each([ @@ -82,8 +111,8 @@ describe('createFlushMiddleware', () => { ); expect(res.statusCode).toBe(401); expect(next).not.toHaveBeenCalled(); - expect(graphileCache.get('tenant.example.com')).toBeDefined(); - expect(svcCache.get('tenant.example.com')).toBeDefined(); + expect(graphileCache.get(SERVICE_KEY)).toBeDefined(); + expect(svcCache.get(SERVICE_KEY)).toBeDefined(); }); it('keeps the route closed when no token is configured', async () => { @@ -94,7 +123,7 @@ describe('createFlushMiddleware', () => { next ); expect(res.statusCode).toBe(404); - expect(graphileCache.get('tenant.example.com')).toBeDefined(); - expect(svcCache.get('tenant.example.com')).toBeDefined(); + expect(graphileCache.get(SERVICE_KEY)).toBeDefined(); + expect(svcCache.get(SERVICE_KEY)).toBeDefined(); }); }); diff --git a/graphql/server/src/middleware/__tests__/graphile-runtime-pool.test.ts b/graphql/server/src/middleware/__tests__/graphile-runtime-pool.test.ts new file mode 100644 index 0000000000..37c59cd119 --- /dev/null +++ b/graphql/server/src/middleware/__tests__/graphile-runtime-pool.test.ts @@ -0,0 +1,352 @@ +jest.mock('graphile-cache', () => { + const actual = jest.requireActual('graphile-cache'); + return { + ...actual, + createGraphileInstance: jest.fn(), + }; +}); + +import type { ApiStructure } from '@constructive-io/express-context'; +import type { ConstructiveOptions } from '@constructive-io/graphql-types'; +import type { NextFunction, Request, Response } from 'express'; +import { + clearGraphileCache, + closeAllCaches, + createGraphileInstance, + graphileCache, + type GraphileCacheEntry, +} from 'graphile-cache'; + +import { clearInFlightMap, graphile } from '../graphile'; + +interface Deferred { + promise: Promise; + resolve(value: T): void; + reject(error: unknown): void; +} + +const deferred = (): Deferred => { + let resolve!: (value: T) => void; + let reject!: (error: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +}; + +interface TestLease { + pool: object; + identity: string; + release: jest.Mock; +} + +interface TestContext { + pool: object; + runtimePoolIdentity: string; + retainRuntimePool: jest.Mock; + useModule: jest.Mock; +} + +const mockedCreate = createGraphileInstance as jest.MockedFunction< + typeof createGraphileInstance +>; + +const apiFor = (databaseId: string, apiId: string): ApiStructure => + ({ + apiId, + databaseId, + dbname: `database-${databaseId}`, + anonRole: 'anonymous', + roleName: 'authenticated', + schema: ['tenant_public'], + domains: [], + isPublic: true, + }) as ApiStructure; + +const makeLease = (identity: string, pool: object): TestLease => ({ + pool, + identity, + release: jest.fn(), +}); + +const makeContext = ( + identity: string, + pool: object, + lease: TestLease, + useModule: jest.Mock = jest.fn().mockResolvedValue(undefined) +): TestContext => ({ + pool, + runtimePoolIdentity: identity, + retainRuntimePool: jest.fn(() => lease), + useModule, +}); + +const makeRequest = ( + serviceKey: string, + api: ApiStructure, + context?: TestContext +): Request => + ({ + api, + svc_key: serviceKey, + constructive: context, + requestId: `${serviceKey}-request`, + }) as unknown as Request; + +const makeResponse = (): Response & { + body?: unknown; + statusCode?: number; +} => { + const response: any = { + headersSent: false, + set: jest.fn(() => response), + status(code: number) { + response.statusCode = code; + return response; + }, + json(body: unknown) { + response.body = body; + response.headersSent = true; + return response; + }, + }; + return response; +}; + +let entryOverrides: Partial = {}; +let createdEntries: GraphileCacheEntry[] = []; + +beforeEach(async () => { + await clearGraphileCache(); + clearInFlightMap(); + entryOverrides = {}; + createdEntries = []; + mockedCreate.mockReset(); + mockedCreate.mockImplementation(async (options) => { + const entry = { + pgl: { release: jest.fn().mockResolvedValue(undefined) }, + serv: {}, + handler: jest.fn( + (_request: Request, _response: Response, _next: NextFunction): void => undefined + ), + httpServer: { listening: false }, + cacheKey: options.cacheKey, + createdAt: Date.now(), + releasePresetServices: jest.fn().mockResolvedValue(undefined), + runtimePoolIdentity: options.runtimePoolIdentity, + logicalServiceKey: options.logicalServiceKey, + releaseRuntimePool: options.releaseRuntimePool, + ...entryOverrides, + } as unknown as GraphileCacheEntry; + createdEntries.push(entry); + return entry; + }); +}); + +afterEach(async () => { + await clearGraphileCache(); + clearInFlightMap(); +}); + +const middleware = (): ReturnType => + graphile({ server: { host: 'example.test' } } as ConstructiveOptions); + +describe('Graphile runtime pool ownership', () => { + it('uses one cache lease for a same-identity cache hit', async () => { + const pool = {}; + const lease = makeLease('pool:a', pool); + const context = makeContext('pool:a', pool, lease); + const requestHandler = middleware(); + + await requestHandler( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + makeResponse(), + jest.fn() as unknown as NextFunction + ); + await requestHandler( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + makeResponse(), + jest.fn() as unknown as NextFunction + ); + + expect(context.retainRuntimePool).toHaveBeenCalledTimes(1); + expect(mockedCreate).toHaveBeenCalledTimes(1); + expect(graphileCache.size).toBe(1); + expect(createdEntries[0]).toMatchObject({ + runtimePoolIdentity: 'pool:a', + logicalServiceKey: 'tenant.example.com', + }); + }); + + it('keeps credential rotations as separate physical entries under one logical key', async () => { + const poolA = {}; + const poolB = {}; + const leaseA = makeLease('pool:a', poolA); + const leaseB = makeLease('pool:b', poolB); + const contextA = makeContext('pool:a', poolA, leaseA); + const contextB = makeContext('pool:b', poolB, leaseB); + const requestHandler = middleware(); + const serviceKey = 'tenant.example.com'; + + await requestHandler( + makeRequest(serviceKey, apiFor('db-a', 'api-a'), contextA), + makeResponse(), + jest.fn() as unknown as NextFunction + ); + await requestHandler( + makeRequest(serviceKey, apiFor('db-a', 'api-a'), contextB), + makeResponse(), + jest.fn() as unknown as NextFunction + ); + + expect(mockedCreate).toHaveBeenCalledTimes(2); + expect(graphileCache.has(JSON.stringify([serviceKey, 'pool:a']))).toBe(true); + expect(graphileCache.has(JSON.stringify([serviceKey, 'pool:b']))).toBe(true); + expect(createdEntries.map((entry) => entry.runtimePoolIdentity)).toEqual([ + 'pool:a', + 'pool:b', + ]); + expect(createdEntries.every((entry) => entry.logicalServiceKey === serviceKey)).toBe(true); + + await clearGraphileCache(); + expect(leaseA.release).toHaveBeenCalledTimes(1); + expect(leaseB.release).toHaveBeenCalledTimes(1); + }); + + it('coalesces requests while compute discovery is pending', async () => { + const pool = {}; + const lease = makeLease('pool:a', pool); + const compute = deferred(); + const useModule = jest.fn(() => compute.promise); + const context = makeContext('pool:a', pool, lease, useModule); + const requestHandler = middleware(); + const firstResponse = makeResponse(); + const secondResponse = makeResponse(); + + const first = requestHandler( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + firstResponse, + jest.fn() as unknown as NextFunction + ); + const second = requestHandler( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + secondResponse, + jest.fn() as unknown as NextFunction + ); + + expect(useModule).toHaveBeenCalledTimes(1); + expect(mockedCreate).not.toHaveBeenCalled(); + + compute.resolve(undefined); + await Promise.all([first, second]); + + expect(mockedCreate).toHaveBeenCalledTimes(1); + expect(createdEntries[0].handler).toHaveBeenCalledTimes(2); + expect(context.retainRuntimePool).toHaveBeenCalledTimes(1); + }); + + it('releases a retained lease when discovery fails and retries on the next request', async () => { + const pool = {}; + const firstLease = makeLease('pool:a', pool); + const secondLease = makeLease('pool:a', pool); + const failure = new Error('compute lookup failed'); + const useModule = jest + .fn() + .mockRejectedValueOnce(failure) + .mockResolvedValue(undefined); + const context = makeContext('pool:a', pool, firstLease, useModule); + context.retainRuntimePool + .mockReturnValueOnce(firstLease) + .mockReturnValueOnce(secondLease); + const requestHandler = middleware(); + const failedResponse = makeResponse(); + + await requestHandler( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + failedResponse, + jest.fn() as unknown as NextFunction + ); + + expect(firstLease.release).toHaveBeenCalledTimes(1); + expect(mockedCreate).not.toHaveBeenCalled(); + expect(failedResponse.statusCode).toBe(200); + + await requestHandler( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + makeResponse(), + jest.fn() as unknown as NextFunction + ); + + expect(mockedCreate).toHaveBeenCalledTimes(1); + expect(graphileCache.size).toBe(1); + await clearGraphileCache(); + expect(secondLease.release).toHaveBeenCalledTimes(1); + }); + + it('does not publish a build that finishes while closeAllCaches is draining it', async () => { + const pool = {}; + const lease = makeLease('pool:a', pool); + const compute = deferred(); + const context = makeContext('pool:a', pool, lease, jest.fn(() => compute.promise)); + const request = middleware()( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + makeResponse(), + jest.fn() as unknown as NextFunction + ); + const closing = closeAllCaches(); + + expect(graphileCache.size).toBe(0); + compute.resolve(undefined); + await Promise.all([request, closing]); + + expect(graphileCache.size).toBe(0); + expect(lease.release).toHaveBeenCalledTimes(1); + expect(createdEntries[0].pgl.release).toHaveBeenCalledTimes(1); + }); + + it('fails closed when runtime pool lease capability is missing or changes identity', async () => { + const missingCapabilityResponse = makeResponse(); + await middleware()( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a')), + missingCapabilityResponse, + jest.fn() as unknown as NextFunction + ); + + const pool = {}; + const lease = makeLease('pool:b', pool); + const changedContext = makeContext('pool:a', pool, lease); + const changedResponse = makeResponse(); + await middleware()( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), changedContext), + changedResponse, + jest.fn() as unknown as NextFunction + ); + + expect(missingCapabilityResponse.statusCode).toBe(200); + expect(changedResponse.statusCode).toBe(200); + expect(mockedCreate).not.toHaveBeenCalled(); + expect(lease.release).toHaveBeenCalledTimes(1); + }); + + it('rejects and disposes an unpublished entry with mismatched runtime ownership metadata', async () => { + const pool = {}; + const lease = makeLease('pool:a', pool); + const context = makeContext('pool:a', pool, lease); + entryOverrides = { runtimePoolIdentity: 'pool:stale' }; + const response = makeResponse(); + + await middleware()( + makeRequest('tenant.example.com', apiFor('db-a', 'api-a'), context), + response, + jest.fn() as unknown as NextFunction + ); + + expect(response.statusCode).toBe(200); + expect(createdEntries[0].handler).not.toHaveBeenCalled(); + expect(graphileCache.size).toBe(0); + expect(lease.release).toHaveBeenCalledTimes(1); + await clearGraphileCache(); + expect(lease.release).toHaveBeenCalledTimes(1); + }); +}); diff --git a/graphql/server/src/middleware/flush.ts b/graphql/server/src/middleware/flush.ts index 635ad2657e..738b2f6aa5 100644 --- a/graphql/server/src/middleware/flush.ts +++ b/graphql/server/src/middleware/flush.ts @@ -12,6 +12,14 @@ import { getRoutingSchema, isValidSchemaName } from './routing'; const log = new Logger('flush'); +// Credential rotations create multiple physical generations for one logical +// service. A service flush retires every such generation. +const flushGraphileService = (serviceKey: string): void => { + graphileCache.forEach((entry, key) => { + if ((entry.logicalServiceKey ?? key) === serviceKey) graphileCache.delete(key); + }); +}; + const bearerToken = (req: Request): string | null => { const header = req.get('authorization'); if (!header) return null; @@ -61,7 +69,7 @@ export const createFlushMiddleware = ( return; } - graphileCache.delete((req as any).svc_key); + flushGraphileService((req as any).svc_key); svcCache.delete((req as any).svc_key); res.status(200).send('OK'); }; @@ -79,9 +87,10 @@ export const flushService = async ( const meta = new RegExp(`^metaschema:api:${databaseId}`); if (!opts.api.isPublic) { - graphileCache.forEach((_, k: string) => { + graphileCache.forEach((entry, cacheKey: string) => { + const k = entry.logicalServiceKey ?? cacheKey; if (api.test(k) || schemata.test(k) || meta.test(k)) { - graphileCache.delete(k); + graphileCache.delete(cacheKey); svcCache.delete(k); } }); @@ -104,7 +113,7 @@ export const flushService = async ( for (const row of svc.rows) { const key: string | undefined = row.hostname || undefined; if (key) { - graphileCache.delete(key); + flushGraphileService(key); svcCache.delete(key); } } diff --git a/graphql/server/src/middleware/graphile.ts b/graphql/server/src/middleware/graphile.ts index cf12e155f1..d1d363615c 100644 --- a/graphql/server/src/middleware/graphile.ts +++ b/graphql/server/src/middleware/graphile.ts @@ -6,12 +6,10 @@ import type { ConstructiveOptions } from '@constructive-io/graphql-types'; import { getNodeEnv } from '@pgpmjs/env'; import { Logger } from '@pgpmjs/logger'; import type { NextFunction, Request, RequestHandler, Response } from 'express'; -import { createGraphileInstance, graphileCache,type GraphileCacheEntry } from 'graphile-cache'; +import { assertGraphileCacheOpen, createGraphileInstance, disposeUncachedEntry, graphileCache, trackGraphileBuild, type GraphileCacheEntry } from 'graphile-cache'; import type { GraphileConfig } from 'graphile-config'; import { createFunctionBindingsPlugin } from 'graphile-function-bindings'; import { createConstructivePreset, makePgService } from 'graphile-settings'; -import { getPgPool } from 'pg-cache'; -import { getPgEnvOptions } from 'pg-env'; import { isGraphqlObservabilityEnabled } from '../diagnostics/observability'; import { HandlerCreationError } from '../errors/api-errors'; @@ -152,8 +150,8 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { respondWithGraphQLError(res, errors.INTERNAL_FAILURE({ details: 'Missing API info' })); return; } - const key = req.svc_key; - if (!key) { + const serviceKey = req.svc_key; + if (!serviceKey) { log.error(`${label} Missing service cache key`); respondWithGraphQLError( res, @@ -161,6 +159,22 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { ); return; } + const context = req.constructive; + if (!context?.runtimePoolIdentity || !context.retainRuntimePool) { + throw new Error('Graphile requires a resolved runtime pool and lease capability'); + } + assertGraphileCacheOpen(); + const key = JSON.stringify([serviceKey, context.runtimePoolIdentity]); + const assertEntryOwnership = (entry: GraphileCacheEntry): void => { + if (entry.runtimePoolIdentity !== context.runtimePoolIdentity || + entry.logicalServiceKey !== serviceKey) { + throw new Error('Graphile cache entry is missing its exact runtime ownership'); + } + }; + const serve = (entry: GraphileCacheEntry): unknown => { + assertEntryOwnership(entry); + return entry.handler(req, res, next); + }; const { dbname, anonRole, roleName, schema } = api; const schemaLabel = schema?.join(',') || 'unknown'; @@ -170,7 +184,7 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { const cached = graphileCache.get(key); if (cached) { log.debug(`${label} PostGraphile cache hit key=${key} db=${dbname} schemas=${schemaLabel}`); - return cached.handler(req, res, next); + return serve(cached); } log.debug(`${label} PostGraphile cache miss key=${key} db=${dbname} schemas=${schemaLabel}`); @@ -183,7 +197,7 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { log.debug(`${label} Coalescing request for PostGraphile[${key}] - waiting for in-flight creation`); try { const instance = await inFlight; - return instance.handler(req, res, next); + return serve(instance); } catch (error) { log.warn(`${label} Coalesced request failed for PostGraphile[${key}], retrying`); // Fall through to Phase C to retry creation @@ -198,7 +212,7 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { const recheckedCache = graphileCache.get(key); if (recheckedCache) { log.debug(`${label} PostGraphile cache hit on re-check key=${key}`); - return recheckedCache.handler(req, res, next); + return serve(recheckedCache); } // Re-check in-flight map (another retry may have started creation) @@ -206,52 +220,74 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { if (retryInFlight) { log.debug(`${label} Re-coalescing request for PostGraphile[${key}]`); const retryInstance = await retryInFlight; - return retryInstance.handler(req, res, next); + return serve(retryInstance); } log.info( `${label} Building PostGraphile v5 handler key=${key} db=${dbname} schemas=${schemaLabel} role=${roleName} anon=${anonRole}` ); - const pgConfig = getPgEnvOptions({ - ...opts.pg, - database: dbname - }); - - // Route through pg-cache so the pool is tracked and can be cleaned up - // properly, preventing leaked connections during database teardown. - const pool = getPgPool(pgConfig); - - // Create promise and store in in-flight map BEFORE try block - const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined; - const preset = buildPreset( - pool, - schema || [], - opts.api?.introspectionRole, - api.databaseSettings, - api.apiId, - compute - ); - const creationPromise = observeGraphileBuild( - { - cacheKey: key, - serviceKey: key, - databaseId: api.databaseId ?? null - }, - () => createGraphileInstance({ - preset, - cacheKey: key, - enableRealtime: api.databaseSettings?.enableRealtime - }), - { enabled: observabilityEnabled } - ); + // Retain synchronously while this request still owns its pool lease. + const cacheLease = context.retainRuntimePool(); + if (cacheLease.identity !== context.runtimePoolIdentity || cacheLease.pool !== context.pool) { + cacheLease.release(); + throw new Error('Graphile runtime pool identity changed before cache acquisition'); + } + let creationPromise: Promise; + try { + creationPromise = trackGraphileBuild(async () => { + let preset: GraphileConfig.Preset | undefined; + let instance: GraphileCacheEntry | undefined; + try { + // The first await is inside the registered factory, so callers + // coalesce even while module discovery is pending. + const compute = api.apiId ? await context.useModule('compute') : undefined; + preset = buildPreset( + cacheLease.pool, schema || [], + opts.api?.introspectionRole, api.databaseSettings, api.apiId, compute + ); + instance = await observeGraphileBuild( + { cacheKey: key, serviceKey, databaseId: api.databaseId ?? null }, + () => createGraphileInstance({ + preset, + cacheKey: key, + runtimePoolIdentity: cacheLease.identity, + logicalServiceKey: serviceKey, + releaseRuntimePool: () => cacheLease.release(), + enableRealtime: api.databaseSettings?.enableRealtime + }), + { enabled: observabilityEnabled } + ); + assertGraphileCacheOpen(); + assertEntryOwnership(instance); + graphileCache.set(key, instance); + return instance; + } catch (error) { + if (instance) { + try { await disposeUncachedEntry(instance); } + catch (cleanupError) { log.error('Failed to dispose unpublished Graphile instance', cleanupError); } + } else { + // Factory failures already release their preset. Public service + // release is idempotent and also covers failures before factory entry. + for (const service of [...(preset?.pgServices ?? [])].reverse()) { + try { await service.release?.(); } + catch (cleanupError) { log.error('Failed to release Graphile build service', cleanupError); } + } + cacheLease.release(); + } + throw error; + } + }); + } catch (error) { + cacheLease.release(); + throw error; + } creating.set(key, creationPromise); try { const instance = await creationPromise; - graphileCache.set(key, instance); log.info(`${label} Cached PostGraphile v5 handler key=${key} db=${dbname}`); - return instance.handler(req, res, next); + return serve(instance); } catch (error) { log.error(`${label} Failed to create PostGraphile[${key}]:`, error); throw new HandlerCreationError( @@ -263,7 +299,7 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => { ); } finally { // Always clean up in-flight tracker - creating.delete(key); + if (creating.get(key) === creationPromise) creating.delete(key); } } catch (e: any) { log.error(`${label} PostGraphile middleware error`, e); diff --git a/graphql/server/src/server.ts b/graphql/server/src/server.ts index c5aa491f37..171c45f04d 100644 --- a/graphql/server/src/server.ts +++ b/graphql/server/src/server.ts @@ -9,7 +9,7 @@ import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils'; import { PgpmOptions } from '@pgpmjs/types'; import cookieParser from 'cookie-parser'; import express, { Express, NextFunction, Request, RequestHandler, Response } from 'express'; -import { closeAllCaches,graphileCache } from 'graphile-cache'; +import { clearGraphileCache, closeAllCaches } from 'graphile-cache'; import graphqlUpload from 'graphql-upload'; import type { Server as HttpServer } from 'http'; import { Pool, PoolClient } from 'pg'; @@ -171,6 +171,9 @@ class Server { app.use(authenticate); app.use(createContextMiddleware({ pg: effectiveOpts.pg, + runtimePg: effectiveOpts.runtimePg, + runtimePgStaticIdentity: effectiveOpts.runtimePgStaticIdentity, + runtimePgResolver: effectiveOpts.runtimePgResolver, dependencySchemas: effectiveOpts.graphile?.introspectionDependencySchemas, loaders: createDefaultRegistry(), routingSchema: getRoutingSchema(effectiveOpts) @@ -392,7 +395,7 @@ class Server { if (closePools) { await closeAllCaches(); } else { - graphileCache.clear(); + await clearGraphileCache(); } } diff --git a/graphql/types/src/constructive.ts b/graphql/types/src/constructive.ts index 485a4f4a59..4c6eba42f0 100644 --- a/graphql/types/src/constructive.ts +++ b/graphql/types/src/constructive.ts @@ -7,7 +7,7 @@ import { PgTestConnectionOptions, ServerOptions} from '@pgpmjs/types'; import deepmerge from 'deepmerge'; -import { PgConfig } from 'pg-env'; +import type { PgConfig, PgPoolConfig } from 'pg-env'; import { apiDefaults, @@ -19,6 +19,24 @@ import { import { LlmOptions } from './llm'; import { SmsOptions } from './sms'; +/** Credential-free route facts used to resolve one runtime login. */ +export interface RuntimePgResolverInput { + databaseId: string; + databaseName: string; + apiId: string; + /** Physical schemas in Graphile exposure order. */ + schemas: readonly string[]; + /** Request roles in `[anonymous, authenticated]` order. */ + roles: readonly [anonymous: string, authenticated: string]; +} + +export type RuntimePgConfig = Partial & { pool?: PgPoolConfig }; + +/** Resolve a least-privilege login from credential-free exact route facts. */ +export type RuntimePgResolver = ( + input: Readonly +) => RuntimePgConfig | Promise; + /** * GraphQL-specific options for Constructive */ @@ -40,6 +58,12 @@ export interface ConstructiveOptions extends PgpmOptions, ConstructiveGraphQLOpt db?: Partial; /** PostgreSQL connection configuration */ pg?: Partial; + /** Static least-privilege tenant execution login. */ + runtimePg?: RuntimePgConfig; + /** Exact route authorized to use the static runtime login. */ + runtimePgStaticIdentity?: RuntimePgResolverInput; + /** Per-route least-privilege tenant execution login resolver. */ + runtimePgResolver?: RuntimePgResolver; /** PostGraphile/Graphile configuration */ graphile?: GraphileOptions; /** HTTP server configuration */ diff --git a/graphql/types/src/index.ts b/graphql/types/src/index.ts index 895604e137..3e66b2d520 100644 --- a/graphql/types/src/index.ts +++ b/graphql/types/src/index.ts @@ -12,7 +12,10 @@ export { constructiveDefaults, constructiveGraphqlDefaults, ConstructiveGraphQLOptions, - ConstructiveOptions} from './constructive'; + ConstructiveOptions, + RuntimePgConfig, + RuntimePgResolver, + RuntimePgResolverInput} from './constructive'; // Export GraphQL adapter types export { diff --git a/packages/express-context/__tests__/context-pg-settings.test.ts b/packages/express-context/__tests__/context-pg-settings.test.ts index a24a21cc0b..24f7c6b22e 100644 --- a/packages/express-context/__tests__/context-pg-settings.test.ts +++ b/packages/express-context/__tests__/context-pg-settings.test.ts @@ -4,6 +4,7 @@ import { buildContext } from '../src/context'; jest.mock('pg-cache', () => ({ getPgPool: jest.fn(() => ({ query: jest.fn(), connect: jest.fn() })), + getPgPoolIdentity: jest.fn(() => 'pg:v1:test'), })); describe('buildContext pgSettings forwarding', () => { diff --git a/packages/express-context/__tests__/context-pool-leases.test.ts b/packages/express-context/__tests__/context-pool-leases.test.ts new file mode 100644 index 0000000000..66e6923248 --- /dev/null +++ b/packages/express-context/__tests__/context-pool-leases.test.ts @@ -0,0 +1,366 @@ +import { EventEmitter } from 'node:events'; + +import type { Request, Response } from 'express'; +import type { Pool } from 'pg'; +import { + acquirePgPool, + getPgDatabaseTargetIdentity, + getPgPool, + getPgPoolIdentity, +} from 'pg-cache'; + +import { buildContext, createContextMiddleware } from '../src/context'; +import type { ApiStructure } from '../src/types'; + +jest.mock('pg-cache', () => ({ + acquirePgPool: jest.fn(), + getPgDatabaseTargetIdentity: jest.fn(() => 'pg-target:test'), + getPgPool: jest.fn(), + getPgPoolIdentity: jest.fn(() => 'pg:v1:test'), +})); + +const mockedAcquire = acquirePgPool as jest.MockedFunction< + typeof acquirePgPool +>; +const mockedGet = getPgPool as jest.MockedFunction; +const mockedTarget = getPgDatabaseTargetIdentity as jest.MockedFunction< + typeof getPgDatabaseTargetIdentity +>; +const mockedIdentity = getPgPoolIdentity as jest.MockedFunction< + typeof getPgPoolIdentity +>; + +const api: ApiStructure = { + apiId: 'api-a', + databaseId: 'database-a', + dbname: 'tenant_a', + anonRole: 'anonymous', + roleName: 'authenticated', + schema: ['tenant_a_public'], + domains: [], + isPublic: false, +}; + +const runtimeRoute = { + databaseId: 'database-a', + databaseName: 'tenant_a', + apiId: 'api-a', + schemas: ['tenant_a_public'], + roles: ['anonymous', 'authenticated'] as [string, string], +}; + +const makeRequest = (): Request => + Object.assign(new EventEmitter(), { + api, + requestId: 'request-a', + get: jest.fn((): undefined => undefined), + aborted: false, + socket: { destroyed: false }, + }) as unknown as Request; + +const makePool = (): Pool => ({ query: jest.fn() }) as unknown as Pool; + +const makeResponse = (): Response => { + const response = new EventEmitter() as EventEmitter & { + destroyed: boolean; + writableEnded: boolean; + }; + response.destroyed = false; + response.writableEnded = false; + return response as unknown as Response; +}; + +let leaseSequence = 0; +const leaseFor = (pool: Pool, identity = 'pg:v1:test') => ({ + pool, + identity: identity || `pg:v1:lease-${++leaseSequence}`, + release: jest.fn(), +}); + +describe('context PostgreSQL identities and lifetimes', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedTarget.mockReturnValue('pg-target:test'); + mockedIdentity.mockReturnValue('pg:v1:test'); + }); + + it('preserves the unconfigured single-login path for direct callers', () => { + const pool = makePool(); + mockedGet.mockReturnValue(pool); + + const context = buildContext(makeRequest(), { pg: { user: 'control' } }); + + expect(context?.pool).toBe(pool); + expect(mockedGet).toHaveBeenCalledWith( + { user: 'control', database: 'tenant_a' }, + { purpose: 'runtime' } + ); + expect(mockedAcquire).not.toHaveBeenCalled(); + }); + + it('pins runtime and control pools until response completion', () => { + const leases = [ + leaseFor(makePool()), + leaseFor(makePool()), + leaseFor(makePool()), + ]; + mockedAcquire + .mockReturnValueOnce(leases[0]) + .mockReturnValueOnce(leases[1]) + .mockReturnValueOnce(leases[2]); + const response = makeResponse(); + const next = jest.fn(); + + createContextMiddleware({ + pg: { database: 'routing' }, + loaders: { resolve: jest.fn() } as any, + })(makeRequest(), response, next); + + expect(next).toHaveBeenCalledWith(); + expect(mockedAcquire).toHaveBeenCalledTimes(3); + response.emit('finish'); + response.emit('close'); + for (const lease of leases) expect(lease.release).toHaveBeenCalledTimes(1); + }); + + it('binds a static runtime login to one exact route', () => { + const runtime = leaseFor(makePool()); + runtime.identity = 'pg:v1:test'; + mockedAcquire.mockReturnValue(runtime); + const next = jest.fn(); + + createContextMiddleware({ + pg: { host: 'control.internal' }, + runtimePg: { + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + runtimePgStaticIdentity: runtimeRoute, + })(makeRequest(), makeResponse(), next); + + expect(next).toHaveBeenCalledWith(); + expect(mockedAcquire).toHaveBeenCalledWith( + { + host: 'control.internal', + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + { purpose: 'runtime' } + ); + }); + + it('rejects a static login when the route identity differs', () => { + const next = jest.fn(); + + createContextMiddleware({ + runtimePg: { + database: 'tenant_a', + user: 'runtime', + password: 'secret', + }, + runtimePgStaticIdentity: { ...runtimeRoute, apiId: 'api-b' }, + })(makeRequest(), makeResponse(), next); + + expect(next.mock.calls[0][0]).toEqual( + expect.objectContaining({ + message: expect.stringContaining('identity does not match'), + }) + ); + expect(mockedAcquire).not.toHaveBeenCalled(); + }); + + it('rejects a runtime config whose physical target differs from control', () => { + mockedTarget + .mockReturnValueOnce('pg-target:control') + .mockReturnValueOnce('pg-target:runtime'); + const next = jest.fn(); + + createContextMiddleware({ + pg: { host: 'control.internal' }, + runtimePg: { + host: 'runtime.internal', + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + runtimePgStaticIdentity: runtimeRoute, + })(makeRequest(), makeResponse(), next); + + expect(next.mock.calls[0][0]).toEqual( + expect.objectContaining({ + message: expect.stringContaining('target does not match'), + }) + ); + expect(next.mock.calls[0][0].message).not.toContain('runtime-secret'); + expect(mockedAcquire).not.toHaveBeenCalled(); + }); + + it('retains runtime only while identity and request lifetime remain valid', () => { + const initial = leaseFor(makePool()); + const retained = leaseFor(makePool()); + mockedAcquire + .mockReturnValueOnce(initial) + .mockReturnValueOnce(retained); + const request = makeRequest(); + const response = makeResponse(); + const next = jest.fn(); + + createContextMiddleware({ + runtimePg: { + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + runtimePgStaticIdentity: runtimeRoute, + })(request, response, next); + + expect(request.constructive?.retainRuntimePool?.()).toBe(retained); + expect(mockedAcquire).toHaveBeenCalledWith( + { + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + { purpose: 'runtime' } + ); + retained.release(); + response.emit('finish'); + }); + + it('releases a retained lease when its identity changes after acquisition', () => { + const initial = leaseFor(makePool()); + const mismatch = leaseFor(makePool(), 'pg:v1:changed'); + mockedAcquire + .mockReturnValueOnce(initial) + .mockReturnValueOnce(mismatch); + const request = makeRequest(); + const response = makeResponse(); + const next = jest.fn(); + + createContextMiddleware({ + runtimePg: { + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + runtimePgStaticIdentity: runtimeRoute, + })(request, response, next); + + expect(() => request.constructive?.retainRuntimePool?.()).toThrow( + 'changed after retention' + ); + expect(mismatch.release).toHaveBeenCalledTimes(1); + response.emit('finish'); + }); + + it('rejects runtime retention after the request has ended', () => { + const initial = leaseFor(makePool()); + mockedAcquire.mockReturnValue(initial); + const request = makeRequest(); + const response = makeResponse(); + const next = jest.fn(); + + createContextMiddleware({ + runtimePg: { + database: 'tenant_a', + user: 'runtime', + password: 'runtime-secret', + }, + runtimePgStaticIdentity: runtimeRoute, + })(request, response, next); + (response as any).writableEnded = true; + + expect(() => request.constructive?.retainRuntimePool?.()).toThrow( + 'after request ended' + ); + response.emit('finish'); + }); + + it('passes frozen credential-free facts to the runtime resolver', () => { + const lease = leaseFor(makePool()); + lease.identity = 'pg:v1:test'; + mockedAcquire.mockReturnValue(lease); + const resolver = jest.fn((input) => { + expect(Object.isFrozen(input)).toBe(true); + expect(Object.isFrozen(input.schemas)).toBe(true); + expect(Object.isFrozen(input.roles)).toBe(true); + expect(input).not.toHaveProperty('password'); + return { + host: 'runtime.internal', + database: input.databaseName, + user: 'runtime', + password: 'resolver-secret', + }; + }); + const request = makeRequest(); + const next = jest.fn(); + + createContextMiddleware({ runtimePgResolver: resolver })( + request, + makeResponse(), + next + ); + + expect(resolver).toHaveBeenCalledWith(runtimeRoute); + expect(next).toHaveBeenCalledWith(); + expect(request.constructive?.runtimePoolIdentity).toBe('pg:v1:test'); + }); + + it('does not fall back when the runtime resolver rejects', async () => { + const failure = new Error('resolver unavailable'); + const next = jest.fn(); + + createContextMiddleware({ + runtimePgResolver: async () => { + throw failure; + }, + })(makeRequest(), makeResponse(), next); + await new Promise((resolve) => setImmediate(resolve)); + + expect(next).toHaveBeenCalledWith(failure); + expect(mockedAcquire).not.toHaveBeenCalled(); + expect(mockedGet).not.toHaveBeenCalled(); + }); + + it.each([ + [ + { database: 'tenant_b', user: 'runtime', password: 'secret' }, + 'does not match', + ], + [{ database: 'tenant_a', user: 'runtime' }, 'explicit password'], + ])('fails closed for an invalid resolved login', (resolved, message) => { + const next = jest.fn(); + + createContextMiddleware({ runtimePgResolver: () => resolved })( + makeRequest(), + makeResponse(), + next + ); + + expect(next.mock.calls[0][0]).toEqual( + expect.objectContaining({ + message: expect.stringContaining(message), + }) + ); + expect(mockedAcquire).not.toHaveBeenCalled(); + }); + + it('releases earlier leases when a later control acquisition fails', () => { + const first = leaseFor(makePool()); + const failure = new Error('capacity'); + mockedAcquire.mockReturnValueOnce(first).mockImplementationOnce(() => { + throw failure; + }); + const next = jest.fn(); + + createContextMiddleware({ + pg: { database: 'routing' }, + loaders: { resolve: jest.fn() } as any, + })(makeRequest(), makeResponse(), next); + + expect(first.release).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(failure); + }); +}); diff --git a/packages/express-context/package.json b/packages/express-context/package.json index cf547395ae..c9e4782135 100644 --- a/packages/express-context/package.json +++ b/packages/express-context/package.json @@ -29,6 +29,7 @@ "test:watch": "jest --watch" }, "dependencies": { + "@constructive-io/graphql-types": "workspace:^", "@constructive-io/url-domains": "workspace:^", "@pgpmjs/env": "workspace:^", "@pgpmjs/logger": "workspace:^", diff --git a/packages/express-context/src/context.ts b/packages/express-context/src/context.ts index 87ece44dca..3ab945dcf1 100644 --- a/packages/express-context/src/context.ts +++ b/packages/express-context/src/context.ts @@ -14,10 +14,22 @@ * route handler can use for tenant-scoped database operations. */ +import type { + RuntimePgConfig, + RuntimePgResolver, + RuntimePgResolverInput, +} from '@constructive-io/graphql-types'; import type { PgpmOptions } from '@pgpmjs/types'; import type { NextFunction, Request, RequestHandler, Response } from 'express'; import type { Pool } from 'pg'; -import { getPgPool } from 'pg-cache'; +import { + acquirePgPool, + getPgDatabaseTargetIdentity, + getPgPool, + getPgPoolIdentity, + type GetPgPoolOptions, + type PgPoolLease, +} from 'pg-cache'; import type { BillingClient } from './billing-client'; import { createBillingClient } from './billing-client'; @@ -25,11 +37,36 @@ import type { LoaderRegistry } from './loaders/registry'; import type { LoaderContext } from './loaders/types'; import { withPgClient as withPgClientFn } from './pg-client'; import { buildPgSettings } from './pg-settings'; -import type { BillingConfig, BuiltinModuleMap, ConstructiveContext, InferenceLogConfig, LlmConfig } from './types'; +import type { + ApiStructure, + BillingConfig, + BuiltinModuleMap, + ConstructiveContext, + InferenceLogConfig, + LlmConfig, +} from './types'; + +type PoolConfig = Parameters[0]; + +/** Public metadata for a resolved runtime pool; credentials remain private. */ +export interface RuntimePgPoolResolution { + poolIdentity: string; +} + +interface RuntimePgPoolResolutionInternal extends RuntimePgPoolResolution { + /** Kept only in the middleware closure; never placed on req.constructive. */ + pgConfig: PoolConfig; +} export interface ContextMiddlewareOptions { /** Base PG options for pool creation (host, port, user, password) */ pg?: PgpmOptions['pg']; + /** Static least-privilege tenant execution login. */ + runtimePg?: RuntimePgConfig; + /** Exact route authorized to use `runtimePg`. */ + runtimePgStaticIdentity?: RuntimePgResolverInput; + /** Resolve one least-privilege login from credential-free route facts. */ + runtimePgResolver?: RuntimePgResolver; /** Module loader registry for per-database cached lookups */ loaders?: LoaderRegistry; /** Routing-plane schema loaders query (defaults to routing_public) */ @@ -38,6 +75,156 @@ export interface ContextMiddlewareOptions { dependencySchemas?: readonly string[]; } +interface ResolvedPool { + pool: Pool; + identity: string; +} + +const resolvePool = ( + config: PoolConfig, + options: GetPgPoolOptions, + leases?: PgPoolLease[] +): ResolvedPool => { + if (!leases) { + return { + pool: getPgPool(config, options), + identity: getPgPoolIdentity(config, options), + }; + } + const lease = acquirePgPool(config, options); + leases.push(lease); + return { pool: lease.pool, identity: lease.identity }; +}; + +const requireRouteFact = (value: unknown, name: string): string => { + if (typeof value !== 'string' || value.length === 0) { + throw new Error(`Runtime PostgreSQL route requires ${name}`); + } + return value; +}; + +const runtimeRouteInput = ( + api: ApiStructure +): Readonly => + Object.freeze({ + databaseId: requireRouteFact(api.databaseId, 'databaseId'), + databaseName: requireRouteFact(api.dbname, 'databaseName'), + apiId: requireRouteFact(api.apiId, 'apiId'), + schemas: Object.freeze([...api.schema]), + roles: Object.freeze([ + requireRouteFact(api.anonRole, 'anonymous role'), + requireRouteFact(api.roleName, 'authenticated role'), + ]) as readonly [string, string], + }); + +const sameRuntimeRoute = ( + left: Readonly, + right: Readonly +): boolean => + left.databaseId === right.databaseId && + left.databaseName === right.databaseName && + left.apiId === right.apiId && + left.schemas.length === right.schemas.length && + left.schemas.every((schema, index) => schema === right.schemas[index]) && + left.roles[0] === right.roles[0] && + left.roles[1] === right.roles[1]; + +const hasOwnString = ( + candidate: object, + field: 'database' | 'user' | 'password' +): boolean => { + if (!Object.prototype.hasOwnProperty.call(candidate, field)) return false; + const value = (candidate as Record)[field]; + return typeof value === 'string' && value.length > 0; +}; + +const requireExplicitRuntimeConfig = ( + candidate: RuntimePgConfig, + route: Readonly, + controlPg: PgpmOptions['pg'] | undefined +): PoolConfig => { + if (!candidate || typeof candidate !== 'object') { + throw new Error( + 'Runtime PostgreSQL resolver returned an invalid configuration' + ); + } + for (const field of ['database', 'user', 'password'] as const) { + if (!hasOwnString(candidate, field)) { + throw new Error( + 'Runtime PostgreSQL configuration requires explicit ' + field + ); + } + } + if (candidate.database !== route.databaseName) { + throw new Error( + 'Runtime PostgreSQL database does not match the resolved route' + ); + } + return { ...controlPg, ...candidate }; +}; + +const makeRuntimeResolution = ( + candidate: RuntimePgConfig, + route: Readonly, + controlPg: PgpmOptions['pg'] | undefined +): Readonly => { + const pgConfig = requireExplicitRuntimeConfig(candidate, route, controlPg); + const controlTarget = getPgDatabaseTargetIdentity({ + ...controlPg, + database: route.databaseName, + }); + const runtimeTarget = getPgDatabaseTargetIdentity(pgConfig); + if (controlTarget !== runtimeTarget) { + throw new Error( + 'Runtime PostgreSQL database target does not match the control target' + ); + } + return Object.freeze({ + pgConfig, + poolIdentity: getPgPoolIdentity(pgConfig, { purpose: 'runtime' }), + }); +}; + +const isPromiseLike = ( + value: T | PromiseLike +): value is PromiseLike => + typeof (value as { then?: unknown } | null)?.then === 'function'; + +const resolveConfiguredRuntime = ( + api: ApiStructure, + opts: ContextMiddlewareOptions +): + | Readonly + | Promise> + | undefined => { + if (opts.runtimePgResolver && opts.runtimePg) { + throw new Error( + 'Configure either runtimePgResolver or runtimePg, not both' + ); + } + if (!opts.runtimePgResolver && !opts.runtimePg) return undefined; + const route = runtimeRouteInput(api); + if (opts.runtimePgResolver) { + const candidate = opts.runtimePgResolver(route); + return isPromiseLike(candidate) + ? Promise.resolve(candidate).then((resolved) => + makeRuntimeResolution(resolved, route, opts.pg) + ) + : makeRuntimeResolution(candidate, route, opts.pg); + } + if (!opts.runtimePgStaticIdentity) { + throw new Error( + 'Static runtime PostgreSQL requires runtimePgStaticIdentity' + ); + } + if (!sameRuntimeRoute(route, opts.runtimePgStaticIdentity)) { + throw new Error( + 'Static runtime PostgreSQL identity does not match the resolved route' + ); + } + return makeRuntimeResolution(opts.runtimePg, route, opts.pg); +}; + /** * Create a `useModule` function bound to the given loader context. * @@ -67,7 +254,14 @@ function createUseModule( */ export function buildContext( req: Request, - opts: ContextMiddlewareOptions = {} + opts: ContextMiddlewareOptions = {}, + /** Internal request lifetime; omitted for backwards-compatible direct use. */ + poolLeases?: PgPoolLease[], + /** Internal async resolver result; raw credentials never enter the request. */ + suppliedRuntimeResolution?: Readonly, + /** Request lifetime predicate supplied by the Express middleware. */ + requestEnded: () => boolean = () => + Boolean(req.aborted || req.socket?.destroyed) ): ConstructiveContext | null { const api = req.api; if (!api) return null; @@ -86,19 +280,57 @@ export function buildContext( dependencySchemas: opts.dependencySchemas, }); - const tenantPool: Pool = getPgPool({ + let runtimeResolution = suppliedRuntimeResolution; + if (!runtimeResolution) { + if (opts.runtimePgResolver) { + throw new Error( + 'runtimePgResolver must be used through createContextMiddleware' + ); + } + const configured = resolveConfiguredRuntime(api, opts); + if (isPromiseLike(configured)) { + throw new Error( + 'runtimePgResolver must be used through createContextMiddleware' + ); + } + runtimeResolution = configured; + } + const runtimeConfig = runtimeResolution?.pgConfig ?? { ...opts.pg, - database: api.dbname - }); + database: api.dbname, + }; + const expectedRuntimeIdentity = + runtimeResolution?.poolIdentity ?? + getPgPoolIdentity(runtimeConfig, { purpose: 'runtime' }); + const runtimePool = resolvePool( + runtimeConfig, + { purpose: 'runtime' }, + poolLeases + ); + if (runtimePool.identity !== expectedRuntimeIdentity) { + throw new Error( + 'Resolved runtime PostgreSQL pool identity changed before context use' + ); + } + const tenantPool = runtimePool.pool; // Build loader context (if registry provided and databaseId known) let loaderCtx: LoaderContext | null = null; if (opts.loaders && api.databaseId) { - const routingPool: Pool = getPgPool(opts.pg); + const routingPool = resolvePool( + opts.pg ?? {}, + { purpose: 'routing-request-control' }, + poolLeases + ); + const controlTenantPool = resolvePool( + { ...opts.pg, database: api.dbname }, + { purpose: 'tenant-request-control' }, + poolLeases + ); loaderCtx = { - routingPool, + routingPool: routingPool.pool, routingSchema: opts.routingSchema, - tenantPool, + tenantPool: controlTenantPool.pool, databaseId: api.databaseId, apiId: api.apiId, dbname: api.dbname @@ -122,6 +354,31 @@ export function buildContext( userId: token?.user_id ?? null, requestId, pool: tenantPool, + runtimePoolIdentity: runtimePool.identity, + retainRuntimePool: () => { + if (requestEnded()) { + throw new Error( + 'Cannot retain runtime PostgreSQL pool after request ended' + ); + } + const beforeIdentity = getPgPoolIdentity( + runtimeConfig, + { purpose: 'runtime' } + ); + if (beforeIdentity !== runtimePool.identity) { + throw new Error( + 'Runtime PostgreSQL pool identity changed before retention' + ); + } + const lease = acquirePgPool(runtimeConfig, { purpose: 'runtime' }); + if (lease.identity !== runtimePool.identity) { + lease.release(); + throw new Error( + 'Runtime PostgreSQL pool identity changed after retention' + ); + } + return lease; + }, withPgClient, useModule, async useBilling() { @@ -187,11 +444,78 @@ export function buildContext( export function createContextMiddleware( opts: ContextMiddlewareOptions = {} ): RequestHandler { - return (req: Request, _res: Response, next: NextFunction): void => { - const ctx = buildContext(req, opts); - if (ctx) { - req.constructive = ctx; + return (req: Request, res: Response, next: NextFunction): void => { + const requestEnded = (): boolean => + Boolean( + req.aborted || + req.socket?.destroyed || + res.destroyed || + res.writableEnded + ); + if (requestEnded()) return; + + const finish = ( + runtimeResolution?: Readonly + ): void => { + if (requestEnded()) return; + const leases: PgPoolLease[] = []; + let released = false; + const releaseLeases = (): void => { + if (released) return; + released = true; + req.removeListener('aborted', releaseLeases); + res.removeListener('finish', releaseLeases); + res.removeListener('close', releaseLeases); + for (const lease of leases.reverse()) lease.release(); + }; + + try { + const ctx = buildContext( + req, + opts, + leases, + runtimeResolution, + requestEnded + ); + if (!ctx) { + releaseLeases(); + next(); + return; + } + req.constructive = ctx; + req.once('aborted', releaseLeases); + res.once('finish', releaseLeases); + res.once('close', releaseLeases); + if (requestEnded()) { + releaseLeases(); + return; + } + next(); + } catch (error) { + releaseLeases(); + next(error); + } + }; + + try { + const api = req.api; + if (!api) { + next(); + return; + } + const resolution = resolveConfiguredRuntime(api, opts); + if (isPromiseLike(resolution)) { + void Promise.resolve(resolution).then( + (resolved) => finish(resolved), + (error) => { + if (!requestEnded()) next(error); + } + ); + } else { + finish(resolution); + } + } catch (error) { + next(error); } - next(); }; } diff --git a/packages/express-context/src/index.ts b/packages/express-context/src/index.ts index 185a2e7d80..fae175c645 100644 --- a/packages/express-context/src/index.ts +++ b/packages/express-context/src/index.ts @@ -147,7 +147,7 @@ export { BoundedCounter, CounterFlusher } from './usage-counter'; export { requestIdMiddleware } from './request-id'; // Context middleware -export type { ContextMiddlewareOptions } from './context'; +export type { ContextMiddlewareOptions, RuntimePgPoolResolution } from './context'; export { buildContext, createContextMiddleware } from './context'; // Module loaders diff --git a/packages/express-context/src/types.ts b/packages/express-context/src/types.ts index ad375bbf35..6b17ffec08 100644 --- a/packages/express-context/src/types.ts +++ b/packages/express-context/src/types.ts @@ -7,6 +7,7 @@ * (PostGraphile, LLM sidecar, etc.) can share the same context. */ +import type { PgPoolLease } from 'pg-cache'; import type { Pool, PoolClient } from 'pg'; import type { BillingClient } from './billing-client'; @@ -320,6 +321,10 @@ export interface ConstructiveContext { requestId: string; /** Tenant database connection pool */ pool: Pool; + /** Opaque identity of the exact runtime connection contract. */ + runtimePoolIdentity: string; + /** Retain a separate runtime lease for a cache-owned handler, when needed. */ + retainRuntimePool?: () => PgPoolLease; /** Execute a function within a tenant-scoped RLS transaction */ withPgClient: WithPgClient; diff --git a/patches/@dataplan__pg@1.1.1.patch b/patches/@dataplan__pg@1.1.1.patch new file mode 100644 index 0000000000..19072afabe --- /dev/null +++ b/patches/@dataplan__pg@1.1.1.patch @@ -0,0 +1,327 @@ +diff --git a/dist/adaptors/pg.d.ts b/dist/adaptors/pg.d.ts +index 2c69e9046a83c61e59b42d9a733ea3051a658f59..22797b5aa5fbf277a71bb1691b200e853f94be95 100644 +--- a/dist/adaptors/pg.d.ts ++++ b/dist/adaptors/pg.d.ts +@@ -96,8 +96,10 @@ export declare class PgSubscriber; + private _chain; + private chain; + } +diff --git a/dist/adaptors/pg.js b/dist/adaptors/pg.js +index afa172c99d66784cbefb9aad271978d87e0d4277..3e9f00c711b545945a6c31e9353043c98028159a 100644 +--- a/dist/adaptors/pg.js ++++ b/dist/adaptors/pg.js +@@ -349,6 +349,22 @@ const adaptor = { + makePgService, + }; + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); ++const pgSubscriberReleaseAbort = Symbol("PgSubscriber release abort"); ++const makePgSubscriberReleaseAbortError = () => { ++ const error = new Error("PgSubscriber released; aborting getClient"); ++ error[pgSubscriberReleaseAbort] = true; ++ return error; ++}; ++const isPgSubscriberReleaseAbortError = (error) => error != null && ++ typeof error === "object" && ++ error[pgSubscriberReleaseAbort] === true; ++const releaseErrorFrom = (errors, message) => { ++ if (errors.length === 0) ++ return null; ++ if (errors.length === 1) ++ return errors[0]; ++ return new AggregateError(errors, message); ++}; + /** + * This class provides helpers for Postgres' LISTEN/NOTIFY pub/sub + * implementation. We aggregate all LISTEN/NOTIFY events so that we can supply +@@ -369,6 +385,8 @@ class PgSubscriber { + this.subscribedTopics = new Set(); + this.listeningClient = null; + this.listeningClientPromise = null; ++ this.listeningClientErrorListener = null; ++ this.releasePromise = null; + // Avoid race conditions by chaining everything + this._chain = Promise.resolve(); + this.pool = pool; +@@ -480,7 +498,7 @@ class PgSubscriber { + } + async syncWithClient(client) { + if (!this.alive) { +- throw new Error("PgSubscriber released; aborting syncWithClient"); ++ throw makePgSubscriberReleaseAbortError(); + } + const expectedTopics = Object.keys(this.topics); + const topicsToAdd = expectedTopics.filter((t) => !this.subscribedTopics.has(t)); +@@ -501,6 +519,10 @@ class PgSubscriber { + } + const client = this.listeningClient; + if (client !== null) { ++ if (this.listeningClientErrorListener) { ++ client.off("error", this.listeningClientErrorListener); ++ this.listeningClientErrorListener = null; ++ } + client.off("notification", this.recordNotification); + client.release(); + this.listeningClient = null; +@@ -525,7 +547,7 @@ class PgSubscriber { + } + getClient() { + if (!this.alive) { +- return Promise.reject(new Error("Released; aborting getClient")); ++ return Promise.reject(makePgSubscriberReleaseAbortError()); + } + if (this.listeningClient) { + return Promise.resolve(this.listeningClient); +@@ -536,34 +558,65 @@ class PgSubscriber { + for (let attempts = 0;; attempts++) { + try { + if (!this.alive) { +- return Promise.reject(new Error("PgSubscriber released; aborting getClient")); ++ throw makePgSubscriberReleaseAbortError(); + } + const logError = (e) => { + console.error(`Error on listening client: ${e}`); + }; + const client = await this.pool.connect(); ++ if (!this.alive) { ++ await client.release(true); ++ throw makePgSubscriberReleaseAbortError(); ++ } + try { + client.on("error", logError); + client.on("notification", this.recordNotification); + await this.syncWithClient(client); ++ if (!this.alive) { ++ throw makePgSubscriberReleaseAbortError(); ++ } + // All good; we can return this client finally! + this.listeningClientPromise = null; + this.listeningClient = client; + client.off("error", logError); +- client.on("error", (e) => { ++ const listeningClientErrorListener = (e) => { + logError(e); + this.resetClient(); +- }); ++ }; ++ this.listeningClientErrorListener = listeningClientErrorListener; ++ client.on("error", listeningClientErrorListener); + return client; + } + catch (e) { +- client.off("error", logError); +- client.off("notification", this.recordNotification); +- client.release(); ++ const cleanupErrors = []; ++ try { ++ client.off("error", logError); ++ } ++ catch (error) { ++ cleanupErrors.push(error); ++ } ++ try { ++ client.off("notification", this.recordNotification); ++ } ++ catch (error) { ++ cleanupErrors.push(error); ++ } ++ try { ++ await client.release(!this.alive); ++ } ++ catch (error) { ++ cleanupErrors.push(error); ++ } ++ if (cleanupErrors.length > 0) { ++ throw releaseErrorFrom([e, ...cleanupErrors], "Failed to release a PgSubscriber client after getClient failed"); ++ } + throw e; + } + } + catch (e) { ++ if (!this.alive) { ++ throw e; ++ } + console.error(`Error with listening client during getClient (attempt ${attempts + 1}): ${e}`); + // Exponential back-off (maximum 30 seconds) + await sleep(Math.min(100 * Math.exp(attempts), 30000)); +@@ -582,15 +635,30 @@ class PgSubscriber { + } + } + release() { +- if (this.alive) { ++ if (this.releasePromise) { ++ return this.releasePromise; ++ } ++ this.releasePromise = (async () => { ++ const errors = []; + this.alive = false; ++ const iteratorPromises = []; + for (const topic of Object.keys(this.topics)) { +- for (const asyncIterableIterator of this.topics[topic]) { ++ for (const asyncIterableIterator of [...this.topics[topic]]) { + if (asyncIterableIterator.return) { +- asyncIterableIterator.return().then(null, grafast_1.noop); ++ try { ++ iteratorPromises.push(Promise.resolve(asyncIterableIterator.return())); ++ } ++ catch (error) { ++ errors.push(error); ++ } + } + else if (asyncIterableIterator.throw) { +- asyncIterableIterator.throw(new Error("Released")).then(null, grafast_1.noop); ++ try { ++ iteratorPromises.push(Promise.resolve(asyncIterableIterator.throw(new Error("Released")))); ++ } ++ catch (error) { ++ errors.push(error); ++ } + } + else { + // What do we do now?! +@@ -600,27 +668,89 @@ class PgSubscriber { + } + delete this.topics[topic]; + } ++ const chain = this._chain; ++ const listeningClientPromise = this.listeningClientPromise; ++ const iteratorResults = await Promise.allSettled(iteratorPromises); ++ for (const result of iteratorResults) { ++ if (result.status === "rejected") { ++ errors.push(result.reason); ++ } ++ } ++ try { ++ await chain; ++ } ++ catch (error) { ++ errors.push(error); ++ } ++ if (listeningClientPromise) { ++ try { ++ await listeningClientPromise; ++ } ++ catch (error) { ++ if (!isPgSubscriberReleaseAbortError(error)) { ++ errors.push(error); ++ } ++ } ++ } + const unlistenAndRelease = async (client) => { ++ const errors = []; ++ try { ++ if (this.listeningClientErrorListener) { ++ client.off("error", this.listeningClientErrorListener); ++ this.listeningClientErrorListener = null; ++ } ++ } ++ catch (error) { ++ errors.push(error); ++ } ++ try { ++ client.off("notification", this.recordNotification); ++ } ++ catch (error) { ++ errors.push(error); ++ } + try { + for (const topic of this.subscribedTopics) { + await client.query(`UNLISTEN ${client.escapeIdentifier(topic)}`); + this.subscribedTopics.delete(topic); + } + } +- catch (_e) { +- // ignore ++ catch (error) { ++ errors.push(error); ++ } ++ finally { ++ this.subscribedTopics.clear(); ++ try { ++ await client.release(errors.length > 0); ++ } ++ catch (error) { ++ errors.push(error); ++ } ++ } ++ const releaseError = releaseErrorFrom(errors, "Failed to release a PgSubscriber client"); ++ if (releaseError) { ++ throw releaseError; + } +- client.release(); + }; + if (this.listeningClient) { +- unlistenAndRelease(this.listeningClient).then(null, grafast_1.noop); ++ const client = this.listeningClient; ++ try { ++ await unlistenAndRelease(client); ++ } ++ catch (error) { ++ errors.push(error); ++ } ++ finally { ++ this.listeningClient = null; ++ } + } +- else if (this.listeningClientPromise) { +- this.listeningClientPromise.then(unlistenAndRelease, () => { +- /* ignore */ +- }); ++ this.listeningClientPromise = null; ++ const releaseError = releaseErrorFrom(errors, "Failed to release a PgSubscriber"); ++ if (releaseError) { ++ throw releaseError; + } +- } ++ })(); ++ return this.releasePromise; + } + chain(callback) { + this._chain = this._chain.then(callback, callback).then(null, grafast_1.noop); +@@ -661,6 +791,7 @@ function makePgService(options) { + pgSubscriber = new PgSubscriber(pool); + releasers.push(() => pgSubscriber.release?.()); + } ++ let releasePromise = null; + const service = { + name, + schemas: Array.isArray(schemas) ? schemas : [schemas ?? "public"], +@@ -675,11 +806,27 @@ function makePgService(options) { + pool, + superuserPool, + }, +- async release() { +- // Release in reverse order +- for (const releaser of [...releasers].reverse()) { +- await releaser(); ++ release() { ++ if (releasePromise) { ++ return releasePromise; + } ++ releasePromise = (async () => { ++ const errors = []; ++ // Release in reverse order, but always attempt every releaser. ++ for (const releaser of [...releasers].reverse()) { ++ try { ++ await releaser(); ++ } ++ catch (error) { ++ errors.push(error); ++ } ++ } ++ const releaseError = releaseErrorFrom(errors, "Failed to release a PgService"); ++ if (releaseError) { ++ throw releaseError; ++ } ++ })(); ++ return releasePromise; + }, + }; + return service; diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000000..f152123a5b --- /dev/null +++ b/patches/README.md @@ -0,0 +1,9 @@ +# Maintained dependency patches + +## @dataplan/pg 1.1.1 + +`@dataplan__pg@1.1.1.patch` makes the public `PgSubscriber.release()` promise await iterator termination, pending connection acquisition, serialized subscription work, `UNLISTEN`, and client return. Failed cleanup destroys the connection and rejects. `makePgService.release()` attempts all reverse releasers, including owned pools, and shares its terminal result across callers. + +The patch also updates the published declaration. It preserves the pinned upstream version. PNPM records the patch content hash in `pnpm-lock.yaml`; frozen installs apply it on development machines and CI. Recreate with `pnpm patch @dataplan/pg@1.1.1` and `pnpm patch-commit `. Reassess this patch when upgrading PostGraphile; remove it only once the upstream release has the same completion contract and the graphile-cache public-boundary tests pass without it. + +Regression tests live in `graphile/graphile-cache`; that package runs in the PostgreSQL CI matrix. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 477e15ef0f..e93b280781 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,11 @@ overrides: packageExtensionsChecksum: sha256-x8B4zkJ4KLRX+yspUWxuggXWlz6zrBLSIh72pNhpPiE= +patchedDependencies: + '@dataplan/pg@1.1.1': + hash: 7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee + path: patches/@dataplan__pg@1.1.1.patch + importers: .: devDependencies: @@ -431,7 +436,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -440,13 +445,13 @@ importers: version: link:../graphile-storage-registry/dist graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@types/node': specifier: ^22.19.11 @@ -460,7 +465,7 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) grafast: specifier: 1.1.2 version: 1.1.2(graphql@16.13.0) @@ -469,7 +474,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -481,7 +486,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@types/node': specifier: ^22.19.11 @@ -522,7 +527,7 @@ importers: version: link:../../postgres/pg-cache/dist postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) devDependencies: '@types/express': specifier: ^5.0.6 @@ -533,6 +538,9 @@ importers: nodemon: specifier: ^3.1.14 version: 3.1.14 + pgsql-test: + specifier: workspace:^ + version: link:../../postgres/pgsql-test/dist ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@25.9.1)(typescript@5.9.3) @@ -542,13 +550,13 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) graphile-build: specifier: 5.1.1 version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -563,7 +571,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(f51a9bb2feb8b06fca136f1c2a1a2f6a) + version: 5.1.4(3cfb8ae93140145a791ab179ae3cec49) devDependencies: '@types/node': specifier: ^22.19.11 @@ -586,7 +594,7 @@ importers: version: link:../../postgres/query-builder/dist '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@pgpmjs/logger': specifier: workspace:^ version: link:../../pgpm/logger/dist @@ -598,7 +606,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -636,7 +644,7 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) grafast: specifier: 1.1.2 version: 1.1.2(graphql@16.13.0) @@ -645,7 +653,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -657,7 +665,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(f282a162d8bd20a217e08c60f5396af8) + version: 5.1.4(9d965dba21fac51f59273c4256716137) devDependencies: '@types/node': specifier: ^22.19.11 @@ -686,7 +694,7 @@ importers: version: link:../../packages/express-context/dist '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) accept-language-parser: specifier: ^1.5.0 version: 1.5.0 @@ -698,7 +706,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -713,7 +721,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(f282a162d8bd20a217e08c60f5396af8) + version: 5.1.4(9d965dba21fac51f59273c4256716137) devDependencies: '@types/accept-language-parser': specifier: ^1.5.4 @@ -751,7 +759,7 @@ importers: version: link:../../packages/llm-env/dist '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) grafast: specifier: 1.1.2 version: 1.1.2(graphql@16.13.0) @@ -760,7 +768,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-cache: specifier: workspace:^ version: link:../graphile-cache/dist @@ -769,7 +777,7 @@ importers: version: 1.1.0 graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 @@ -778,7 +786,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@types/node': specifier: ^22.19.11 @@ -804,7 +812,7 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) grafast: specifier: 1.1.2 version: 1.1.2(graphql@16.13.0) @@ -813,7 +821,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -825,7 +833,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@types/node': specifier: ^22.19.11 @@ -851,7 +859,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -863,7 +871,7 @@ importers: version: 16.13.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(f282a162d8bd20a217e08c60f5396af8) + version: 5.1.4(9d965dba21fac51f59273c4256716137) devDependencies: '@types/node': specifier: ^22.19.11 @@ -877,7 +885,7 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) grafast: specifier: 1.1.2 version: 1.1.2(graphql@16.13.0) @@ -886,7 +894,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -901,7 +909,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@types/node': specifier: ^22.19.11 @@ -924,13 +932,13 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) graphile-build: specifier: 5.1.1 version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -942,7 +950,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(f282a162d8bd20a217e08c60f5396af8) + version: 5.1.4(9d965dba21fac51f59273c4256716137) devDependencies: '@types/node': specifier: ^22.19.11 @@ -956,7 +964,7 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) grafast: specifier: 1.1.2 version: 1.1.2(graphql@16.13.0) @@ -965,7 +973,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -977,7 +985,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(f51a9bb2feb8b06fca136f1c2a1a2f6a) + version: 5.1.4(3cfb8ae93140145a791ab179ae3cec49) devDependencies: '@types/geojson': specifier: ^7946.0.14 @@ -1021,7 +1029,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1033,7 +1041,7 @@ importers: version: link:../graphile-storage-registry/dist graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 @@ -1045,7 +1053,7 @@ importers: version: link:../../uploads/mime-bytes/dist postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@constructive-io/s3-utils': specifier: workspace:^ @@ -1068,7 +1076,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1083,7 +1091,7 @@ importers: version: 8.21.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) devDependencies: '@types/pg': specifier: ^8.20.4 @@ -1112,19 +1120,19 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(138876491c004694bff1843ee79c8872) + version: 5.1.4(92324343608439594bbe299323c80ef9) devDependencies: '@types/node': specifier: ^22.19.11 @@ -1144,7 +1152,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1190,7 +1198,7 @@ importers: version: link:../../postgres/pgsql-test/dist postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) publishDirectory: dist graphile/graphile-schema: @@ -1232,13 +1240,13 @@ importers: dependencies: '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) graphile-build: specifier: 5.1.1 version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1253,7 +1261,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(f51a9bb2feb8b06fca136f1c2a1a2f6a) + version: 5.1.4(3cfb8ae93140145a791ab179ae3cec49) devDependencies: '@types/node': specifier: ^22.19.11 @@ -1309,7 +1317,7 @@ importers: version: 1.0.1(grafast@1.1.2(graphql@16.13.0)) '@dataplan/pg': specifier: 1.1.1 - version: 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + version: 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@graphile-contrib/pg-many-to-many': specifier: 2.0.0-rc.2 version: 2.0.0-rc.2 @@ -1342,7 +1350,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-bulk-mutations: specifier: workspace:^ version: link:../graphile-bulk-mutations/dist @@ -1387,7 +1395,7 @@ importers: version: link:../graphile-upload-plugin/dist graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 @@ -1408,7 +1416,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) request-ip: specifier: ^3.3.0 version: 3.3.0 @@ -1452,7 +1460,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1503,7 +1511,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1521,7 +1529,7 @@ importers: version: link:../../postgres/pgsql-test/dist postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) devDependencies: '@types/pg': specifier: ^8.20.4 @@ -1541,7 +1549,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1550,7 +1558,7 @@ importers: version: 16.13.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(f51a9bb2feb8b06fca136f1c2a1a2f6a) + version: 5.1.4(3cfb8ae93140145a791ab179ae3cec49) devDependencies: '@types/node': specifier: ^22.19.11 @@ -1692,7 +1700,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-cache: specifier: workspace:^ version: link:../../graphile/graphile-cache/dist @@ -1704,7 +1712,7 @@ importers: version: link:../../graphile/graphile-settings/dist graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 @@ -1722,7 +1730,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) devDependencies: '@types/express': specifier: ^5.0.6 @@ -1795,7 +1803,7 @@ importers: version: link:../../postgres/pg-env/dist postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) devDependencies: '@types/express': specifier: ^5.0.6 @@ -1939,7 +1947,7 @@ importers: version: 1.1.2(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -1960,7 +1968,7 @@ importers: version: 11.2.7 postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) devDependencies: makage: specifier: ^0.8.0 @@ -1980,7 +1988,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -2004,7 +2012,7 @@ importers: version: 8.20.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(cf7ef086788d1d3f644d1dc7345cb344) + version: 5.1.4(fdde9aa28a0299d2757f9ae98191ef07) ws: specifier: ^8.20.0 version: 8.20.1 @@ -2093,7 +2101,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-cache: specifier: workspace:^ version: link:../../graphile/graphile-cache/dist @@ -2108,7 +2116,7 @@ importers: version: link:../../graphile/graphile-settings/dist graphile-utils: specifier: 5.0.3 - version: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + version: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: specifier: 16.13.0 version: 16.13.0 @@ -2135,7 +2143,7 @@ importers: version: 5.0.1 postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) request-ip: specifier: ^3.3.0 version: 3.3.0 @@ -2292,7 +2300,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -2316,7 +2324,7 @@ importers: version: link:../../postgres/pgsql-test/dist postgraphile: specifier: 5.1.4 - version: 5.1.4(b3fb9399b7b000ca872a8f427cb73d08) + version: 5.1.4(278bb7eee576e8287d20e8783c1ec9ea) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -2608,6 +2616,9 @@ importers: packages/express-context: dependencies: + '@constructive-io/graphql-types': + specifier: workspace:^ + version: link:../../graphql/types/dist '@constructive-io/url-domains': specifier: workspace:^ version: link:../url-domains/dist @@ -2721,7 +2732,7 @@ importers: version: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) graphile-build-pg: specifier: 5.1.3 - version: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + version: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: specifier: 1.1.0 version: 1.1.0 @@ -2733,7 +2744,7 @@ importers: version: 8.21.0 postgraphile: specifier: 5.1.4 - version: 5.1.4(f282a162d8bd20a217e08c60f5396af8) + version: 5.1.4(9d965dba21fac51f59273c4256716137) devDependencies: '@types/node': specifier: ^22.19.11 @@ -16308,7 +16319,7 @@ snapshots: grafast: 1.1.2(graphql@16.13.0) tslib: 2.8.1 - '@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)': + '@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)': dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) '@graphile/lru': 5.0.0 @@ -16328,7 +16339,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)': + '@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)': dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) '@graphile/lru': 5.0.0 @@ -20413,9 +20424,9 @@ snapshots: - supports-color - use-sync-external-store - graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1): + graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1): dependencies: - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0) '@types/node': 22.19.19 debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) @@ -20432,9 +20443,9 @@ snapshots: transitivePeerDependencies: - supports-color - graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1): + graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1): dependencies: - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@types/node': 22.19.19 debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) @@ -20481,9 +20492,9 @@ snapshots: transitivePeerDependencies: - supports-color - graphile-utils@5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1): + graphile-utils@5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1): dependencies: - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0) debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) @@ -20493,13 +20504,13 @@ snapshots: tamedevil: 0.1.1 tslib: 2.8.1 optionalDependencies: - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1) transitivePeerDependencies: - supports-color - graphile-utils@5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1): + graphile-utils@5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1): dependencies: - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) @@ -20509,7 +20520,7 @@ snapshots: tamedevil: 0.1.1 tslib: 2.8.1 optionalDependencies: - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) transitivePeerDependencies: - supports-color @@ -22511,20 +22522,20 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgraphile@5.1.4(138876491c004694bff1843ee79c8872): + postgraphile@5.1.4(278bb7eee576e8287d20e8783c1ec9ea): dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@graphile/lru': 5.0.0 '@types/node': 22.19.19 '@types/pg': 8.20.4 debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) - grafserv: 1.0.1(@types/node@22.19.15)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) + grafserv: 1.0.1(@types/node@25.9.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: 1.1.0 - graphile-utils: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + graphile-utils: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: 16.13.0 iterall: 1.3.0 jsonwebtoken: 9.0.3 @@ -22538,20 +22549,20 @@ snapshots: - supports-color - utf-8-validate - postgraphile@5.1.4(b3fb9399b7b000ca872a8f427cb73d08): + postgraphile@5.1.4(3cfb8ae93140145a791ab179ae3cec49): dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@graphile/lru': 5.0.0 '@types/node': 22.19.19 '@types/pg': 8.20.4 debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) - grafserv: 1.0.1(@types/node@25.9.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) + grafserv: 1.0.1(@types/node@22.19.11)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: 1.1.0 - graphile-utils: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + graphile-utils: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: 16.13.0 iterall: 1.3.0 jsonwebtoken: 9.0.3 @@ -22565,24 +22576,24 @@ snapshots: - supports-color - utf-8-validate - postgraphile@5.1.4(cf7ef086788d1d3f644d1dc7345cb344): + postgraphile@5.1.4(92324343608439594bbe299323c80ef9): dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@graphile/lru': 5.0.0 '@types/node': 22.19.19 '@types/pg': 8.20.4 debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) - grafserv: 1.0.1(@types/node@25.9.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) + grafserv: 1.0.1(@types/node@22.19.15)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: 1.1.0 - graphile-utils: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + graphile-utils: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: 16.13.0 iterall: 1.3.0 jsonwebtoken: 9.0.3 - pg: 8.20.0 + pg: 8.21.0 pg-sql2: 5.0.1 tamedevil: 0.1.1 tslib: 2.8.1 @@ -22592,10 +22603,10 @@ snapshots: - supports-color - utf-8-validate - postgraphile@5.1.4(f282a162d8bd20a217e08c60f5396af8): + postgraphile@5.1.4(9d965dba21fac51f59273c4256716137): dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) '@graphile/lru': 5.0.0 '@types/node': 22.19.19 '@types/pg': 8.20.4 @@ -22603,9 +22614,9 @@ snapshots: grafast: 1.1.2(graphql@16.13.0) grafserv: 1.0.1(@types/node@22.19.19)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) graphile-config: 1.1.0 - graphile-utils: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + graphile-utils: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: 16.13.0 iterall: 1.3.0 jsonwebtoken: 9.0.3 @@ -22619,24 +22630,24 @@ snapshots: - supports-color - utf-8-validate - postgraphile@5.1.4(f51a9bb2feb8b06fca136f1c2a1a2f6a): + postgraphile@5.1.4(fdde9aa28a0299d2757f9ae98191ef07): dependencies: '@dataplan/json': 1.0.1(grafast@1.1.2(graphql@16.13.0)) - '@dataplan/pg': 1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0) + '@dataplan/pg': 1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0) '@graphile/lru': 5.0.0 '@types/node': 22.19.19 '@types/pg': 8.20.4 debug: 4.4.3(supports-color@5.5.0) grafast: 1.1.2(graphql@16.13.0) - grafserv: 1.0.1(@types/node@22.19.11)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) + grafserv: 1.0.1(@types/node@25.9.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(ws@8.20.1) graphile-build: 5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0) - graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1) + graphile-build-pg: 5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1) graphile-config: 1.1.0 - graphile-utils: 5.0.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.21.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) + graphile-utils: 5.0.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build-pg@5.1.3(@dataplan/pg@1.1.1(patch_hash=7daa6b70ceb989edcb723554bf795df5f363de5f850d0bb2deb9b7911fa4ebee)(@dataplan/json@1.0.1(grafast@1.1.2(graphql@16.13.0)))(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0))(grafast@1.1.2(graphql@16.13.0))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.20.0)(tamedevil@0.1.1))(graphile-build@5.1.1(grafast@1.1.2(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0))(graphile-config@1.1.0)(graphql@16.13.0)(tamedevil@0.1.1) graphql: 16.13.0 iterall: 1.3.0 jsonwebtoken: 9.0.3 - pg: 8.21.0 + pg: 8.20.0 pg-sql2: 5.0.1 tamedevil: 0.1.1 tslib: 2.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b4da2891a3..dd8ea4ff1f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -105,3 +105,6 @@ packageExtensions: allowBuilds: "@launchql/protobufjs": true core-js-pure: true + +patchedDependencies: + '@dataplan/pg@1.1.1': patches/@dataplan__pg@1.1.1.patch