graphile-realtime-subscriptions 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +92 -0
- package/esm/index.d.ts +21 -0
- package/esm/index.js +20 -0
- package/esm/plugin.d.ts +25 -0
- package/esm/plugin.js +120 -0
- package/esm/preset.d.ts +10 -0
- package/esm/preset.js +13 -0
- package/esm/types.d.ts +9 -0
- package/esm/types.js +1 -0
- package/index.d.ts +21 -0
- package/index.js +26 -0
- package/package.json +59 -0
- package/plugin.d.ts +25 -0
- package/plugin.js +123 -0
- package/preset.d.ts +10 -0
- package/preset.js +16 -0
- package/types.d.ts +9 -0
- package/types.js +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
Copyright (c) 2025 Constructive <developers@constructive.io>
|
|
5
|
+
Copyright (c) 2020-present, Interweb, Inc.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# graphile-realtime-subscriptions
|
|
2
|
+
|
|
3
|
+
Realtime subscription plugin for PostGraphile v5 — per-table GraphQL subscriptions via LISTEN/NOTIFY.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Discovers tables tagged with `@realtime` and generates per-table GraphQL subscription fields (`onXxxChanged`) that use PostgreSQL LISTEN/NOTIFY for real-time event delivery with automatic RLS enforcement.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { RealtimeSubscriptionsPreset } from 'graphile-realtime-subscriptions';
|
|
13
|
+
|
|
14
|
+
const preset = {
|
|
15
|
+
extends: [
|
|
16
|
+
RealtimeSubscriptionsPreset(),
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+
1. A row is inserted/updated/deleted on a `@realtime`-tagged table
|
|
24
|
+
2. The `emit_change` trigger fires `pg_notify('realtime:{schema}.{table}', TG_OP)`
|
|
25
|
+
3. PostGraphile's `pgSubscriber` receives the NOTIFY
|
|
26
|
+
4. The subscription re-queries the source table with RLS enforced
|
|
27
|
+
5. The client receives `{ event, row }` where `row` reflects the current state
|
|
28
|
+
|
|
29
|
+
## Subscription Modes
|
|
30
|
+
|
|
31
|
+
### Phase 3a (current)
|
|
32
|
+
|
|
33
|
+
- **Single record**: `onXxxChanged(id: UUID!)` — subscribe to changes on one row
|
|
34
|
+
- **Full collection**: `onXxxChanged` (no args) — subscribe to any change on the table
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Education and Tutorials
|
|
39
|
+
|
|
40
|
+
1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
|
|
41
|
+
Get started with modular databases in minutes. Install prerequisites and deploy your first module.
|
|
42
|
+
|
|
43
|
+
2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
|
|
44
|
+
Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
|
|
45
|
+
|
|
46
|
+
3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
|
|
47
|
+
Master the workflow for adding, organizing, and managing database changes with pgpm.
|
|
48
|
+
|
|
49
|
+
4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
|
|
50
|
+
Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
|
|
51
|
+
|
|
52
|
+
5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
|
|
53
|
+
Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
|
|
54
|
+
|
|
55
|
+
6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
|
|
56
|
+
Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
|
|
57
|
+
|
|
58
|
+
7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
|
|
59
|
+
Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
60
|
+
|
|
61
|
+
## Related Constructive Tooling
|
|
62
|
+
|
|
63
|
+
### 📦 Package Management
|
|
64
|
+
|
|
65
|
+
* [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
|
|
66
|
+
|
|
67
|
+
### 🧪 Testing
|
|
68
|
+
|
|
69
|
+
* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
|
|
70
|
+
* [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
|
|
71
|
+
* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
|
|
72
|
+
* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
|
|
73
|
+
* [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
|
|
74
|
+
|
|
75
|
+
### 🧠 Parsing & AST
|
|
76
|
+
|
|
77
|
+
* [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
|
|
78
|
+
* [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
|
|
79
|
+
* [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
|
|
80
|
+
* [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
|
|
81
|
+
* [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
|
|
82
|
+
* [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
|
|
83
|
+
|
|
84
|
+
## Credits
|
|
85
|
+
|
|
86
|
+
**🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
|
|
87
|
+
|
|
88
|
+
## Disclaimer
|
|
89
|
+
|
|
90
|
+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
|
|
91
|
+
|
|
92
|
+
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
3
|
+
*
|
|
4
|
+
* Adds per-table GraphQL subscription fields to tables tagged with @realtime.
|
|
5
|
+
* Uses PostgreSQL LISTEN/NOTIFY for efficient event delivery with automatic
|
|
6
|
+
* RLS enforcement on re-queries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { RealtimeSubscriptionsPreset } from 'graphile-realtime-subscriptions';
|
|
11
|
+
*
|
|
12
|
+
* const preset = {
|
|
13
|
+
* extends: [
|
|
14
|
+
* RealtimeSubscriptionsPreset(),
|
|
15
|
+
* ],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export { createRealtimeSubscriptionsPlugin, RealtimeSubscriptionsPlugin } from './plugin';
|
|
20
|
+
export { RealtimeSubscriptionsPreset } from './preset';
|
|
21
|
+
export type { RealtimeSubscriptionsPluginOptions } from './types';
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
3
|
+
*
|
|
4
|
+
* Adds per-table GraphQL subscription fields to tables tagged with @realtime.
|
|
5
|
+
* Uses PostgreSQL LISTEN/NOTIFY for efficient event delivery with automatic
|
|
6
|
+
* RLS enforcement on re-queries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { RealtimeSubscriptionsPreset } from 'graphile-realtime-subscriptions';
|
|
11
|
+
*
|
|
12
|
+
* const preset = {
|
|
13
|
+
* extends: [
|
|
14
|
+
* RealtimeSubscriptionsPreset(),
|
|
15
|
+
* ],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export { createRealtimeSubscriptionsPlugin, RealtimeSubscriptionsPlugin } from './plugin';
|
|
20
|
+
export { RealtimeSubscriptionsPreset } from './preset';
|
package/esm/plugin.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
3
|
+
*
|
|
4
|
+
* Discovers tables tagged with @realtime and generates per-table
|
|
5
|
+
* subscription fields (onXxxChanged) that use PostgreSQL LISTEN/NOTIFY
|
|
6
|
+
* for real-time event delivery.
|
|
7
|
+
*
|
|
8
|
+
* Subscription modes (Phase 3a):
|
|
9
|
+
* - Single record: onXxxChanged(id: UUID!) — subscribe to changes on one row
|
|
10
|
+
* - Full collection: onXxxChanged (no args) — subscribe to any change on the table
|
|
11
|
+
*
|
|
12
|
+
* Event flow:
|
|
13
|
+
* 1. A row is inserted/updated/deleted
|
|
14
|
+
* 2. The emit_change trigger fires pg_notify('realtime:{schema}.{table}', TG_OP)
|
|
15
|
+
* 3. PostGraphile's pgSubscriber receives the NOTIFY
|
|
16
|
+
* 4. The subscription re-queries the source table with RLS enforced
|
|
17
|
+
* 5. The client receives { event, row } where row reflects the current state
|
|
18
|
+
*
|
|
19
|
+
* RLS enforcement is automatic — resource.get() queries through the
|
|
20
|
+
* authenticated user's connection with their JWT role applied.
|
|
21
|
+
*/
|
|
22
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
23
|
+
import type { RealtimeSubscriptionsPluginOptions } from './types';
|
|
24
|
+
export declare function createRealtimeSubscriptionsPlugin(_options?: RealtimeSubscriptionsPluginOptions): GraphileConfig.Plugin;
|
|
25
|
+
export { createRealtimeSubscriptionsPlugin as RealtimeSubscriptionsPlugin };
|
package/esm/plugin.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
3
|
+
*
|
|
4
|
+
* Discovers tables tagged with @realtime and generates per-table
|
|
5
|
+
* subscription fields (onXxxChanged) that use PostgreSQL LISTEN/NOTIFY
|
|
6
|
+
* for real-time event delivery.
|
|
7
|
+
*
|
|
8
|
+
* Subscription modes (Phase 3a):
|
|
9
|
+
* - Single record: onXxxChanged(id: UUID!) — subscribe to changes on one row
|
|
10
|
+
* - Full collection: onXxxChanged (no args) — subscribe to any change on the table
|
|
11
|
+
*
|
|
12
|
+
* Event flow:
|
|
13
|
+
* 1. A row is inserted/updated/deleted
|
|
14
|
+
* 2. The emit_change trigger fires pg_notify('realtime:{schema}.{table}', TG_OP)
|
|
15
|
+
* 3. PostGraphile's pgSubscriber receives the NOTIFY
|
|
16
|
+
* 4. The subscription re-queries the source table with RLS enforced
|
|
17
|
+
* 5. The client receives { event, row } where row reflects the current state
|
|
18
|
+
*
|
|
19
|
+
* RLS enforcement is automatic — resource.get() queries through the
|
|
20
|
+
* authenticated user's connection with their JWT role applied.
|
|
21
|
+
*/
|
|
22
|
+
import { context as grafastContext, listen, object, constant } from 'grafast';
|
|
23
|
+
import { extendSchema } from 'graphile-utils';
|
|
24
|
+
import { Logger } from '@pgpmjs/logger';
|
|
25
|
+
const log = new Logger('graphile-realtime-subscriptions');
|
|
26
|
+
function discoverRealtimeTables(build) {
|
|
27
|
+
const { pgRegistry } = build.input;
|
|
28
|
+
const resources = pgRegistry.pgResources;
|
|
29
|
+
const result = [];
|
|
30
|
+
for (const [, resource] of Object.entries(resources)) {
|
|
31
|
+
const r = resource;
|
|
32
|
+
const codec = r.codec;
|
|
33
|
+
if (!codec?.attributes)
|
|
34
|
+
continue;
|
|
35
|
+
const tags = codec.extensions?.tags;
|
|
36
|
+
if (!tags?.realtime)
|
|
37
|
+
continue;
|
|
38
|
+
const typeName = build.inflection.tableType(codec);
|
|
39
|
+
const fieldName = `on${typeName}Changed`;
|
|
40
|
+
const payloadTypeName = `${typeName}SubscriptionPayload`;
|
|
41
|
+
const rowFieldName = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
|
42
|
+
const pgSchema = codec.extensions?.pg?.schemaName ?? 'public';
|
|
43
|
+
const pgTable = codec.extensions?.pg?.name ?? codec.name;
|
|
44
|
+
const notifyChannel = `realtime:${pgSchema}.${pgTable}`;
|
|
45
|
+
result.push({
|
|
46
|
+
resource: r,
|
|
47
|
+
typeName,
|
|
48
|
+
fieldName,
|
|
49
|
+
payloadTypeName,
|
|
50
|
+
rowFieldName,
|
|
51
|
+
notifyChannel,
|
|
52
|
+
pgSchema,
|
|
53
|
+
pgTable,
|
|
54
|
+
});
|
|
55
|
+
log.info(`Discovered realtime table: ${pgSchema}.${pgTable} -> ${fieldName}`);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function buildTypeDefs(tables) {
|
|
60
|
+
const subscriptionFields = tables
|
|
61
|
+
.map(({ fieldName, payloadTypeName }) => ` """Subscribe to changes on this table. Pass an id to watch a specific record."""\n ${fieldName}(id: UUID): ${payloadTypeName}`)
|
|
62
|
+
.join('\n');
|
|
63
|
+
const payloadTypes = tables
|
|
64
|
+
.map(({ payloadTypeName, typeName, rowFieldName }) => `"""Payload delivered when a ${typeName} row changes."""\n` +
|
|
65
|
+
`type ${payloadTypeName} {\n` +
|
|
66
|
+
` """The DML operation: INSERT, UPDATE, or DELETE."""\n` +
|
|
67
|
+
` event: String!\n` +
|
|
68
|
+
` """The current state of the row (null for DELETE or if RLS denies access)."""\n` +
|
|
69
|
+
` ${rowFieldName}: ${typeName}\n` +
|
|
70
|
+
`}`)
|
|
71
|
+
.join('\n\n');
|
|
72
|
+
return `extend type Subscription {\n${subscriptionFields}\n}\n\n${payloadTypes}`;
|
|
73
|
+
}
|
|
74
|
+
function buildPlans(tables) {
|
|
75
|
+
const subscriptionPlans = {};
|
|
76
|
+
const allPlans = {};
|
|
77
|
+
for (const { resource, fieldName, payloadTypeName, rowFieldName, notifyChannel } of tables) {
|
|
78
|
+
subscriptionPlans[fieldName] = {
|
|
79
|
+
subscribePlan(_$root, args) {
|
|
80
|
+
const $pgSubscriber = grafastContext().get('pgSubscriber');
|
|
81
|
+
const $topic = constant(notifyChannel);
|
|
82
|
+
const $id = args.get('id');
|
|
83
|
+
return listen($pgSubscriber, $topic, ($payload) => {
|
|
84
|
+
return object({
|
|
85
|
+
event: $payload,
|
|
86
|
+
subscribedId: $id,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
plan($event) {
|
|
91
|
+
return $event;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
allPlans[payloadTypeName] = {
|
|
95
|
+
event($parent) {
|
|
96
|
+
return $parent.get('event');
|
|
97
|
+
},
|
|
98
|
+
[rowFieldName]($parent) {
|
|
99
|
+
const $id = $parent.get('subscribedId');
|
|
100
|
+
return resource.get({ id: $id });
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
allPlans['Subscription'] = subscriptionPlans;
|
|
105
|
+
return allPlans;
|
|
106
|
+
}
|
|
107
|
+
export function createRealtimeSubscriptionsPlugin(_options = {}) {
|
|
108
|
+
return extendSchema((build) => {
|
|
109
|
+
const tables = discoverRealtimeTables(build);
|
|
110
|
+
if (tables.length === 0) {
|
|
111
|
+
log.info('No tables with @realtime tag found — skipping subscription generation');
|
|
112
|
+
return { typeDefs: '', plans: {} };
|
|
113
|
+
}
|
|
114
|
+
log.info(`Generating subscription fields for ${tables.length} realtime table(s)`);
|
|
115
|
+
const typeDefs = buildTypeDefs(tables);
|
|
116
|
+
const plans = buildPlans(tables);
|
|
117
|
+
return { typeDefs, plans };
|
|
118
|
+
}, 'RealtimeSubscriptionsPlugin');
|
|
119
|
+
}
|
|
120
|
+
export { createRealtimeSubscriptionsPlugin as RealtimeSubscriptionsPlugin };
|
package/esm/preset.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Realtime Subscriptions Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset for including realtime subscription support
|
|
5
|
+
* in PostGraphile. Wraps the RealtimeSubscriptionsPlugin with options.
|
|
6
|
+
*/
|
|
7
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
8
|
+
import type { RealtimeSubscriptionsPluginOptions } from './types';
|
|
9
|
+
export declare function RealtimeSubscriptionsPreset(options?: RealtimeSubscriptionsPluginOptions): GraphileConfig.Preset;
|
|
10
|
+
export default RealtimeSubscriptionsPreset;
|
package/esm/preset.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Realtime Subscriptions Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset for including realtime subscription support
|
|
5
|
+
* in PostGraphile. Wraps the RealtimeSubscriptionsPlugin with options.
|
|
6
|
+
*/
|
|
7
|
+
import { createRealtimeSubscriptionsPlugin } from './plugin';
|
|
8
|
+
export function RealtimeSubscriptionsPreset(options = {}) {
|
|
9
|
+
return {
|
|
10
|
+
plugins: [createRealtimeSubscriptionsPlugin(options)],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export default RealtimeSubscriptionsPreset;
|
package/esm/types.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the Realtime Subscriptions Plugin.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3a supports single-record and full-collection subscription modes
|
|
5
|
+
* with no additional configuration required. Future phases will add
|
|
6
|
+
* options for poll intervals, batch sizes, and throttling.
|
|
7
|
+
*/
|
|
8
|
+
export interface RealtimeSubscriptionsPluginOptions {
|
|
9
|
+
}
|
package/esm/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
3
|
+
*
|
|
4
|
+
* Adds per-table GraphQL subscription fields to tables tagged with @realtime.
|
|
5
|
+
* Uses PostgreSQL LISTEN/NOTIFY for efficient event delivery with automatic
|
|
6
|
+
* RLS enforcement on re-queries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { RealtimeSubscriptionsPreset } from 'graphile-realtime-subscriptions';
|
|
11
|
+
*
|
|
12
|
+
* const preset = {
|
|
13
|
+
* extends: [
|
|
14
|
+
* RealtimeSubscriptionsPreset(),
|
|
15
|
+
* ],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export { createRealtimeSubscriptionsPlugin, RealtimeSubscriptionsPlugin } from './plugin';
|
|
20
|
+
export { RealtimeSubscriptionsPreset } from './preset';
|
|
21
|
+
export type { RealtimeSubscriptionsPluginOptions } from './types';
|
package/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
4
|
+
*
|
|
5
|
+
* Adds per-table GraphQL subscription fields to tables tagged with @realtime.
|
|
6
|
+
* Uses PostgreSQL LISTEN/NOTIFY for efficient event delivery with automatic
|
|
7
|
+
* RLS enforcement on re-queries.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { RealtimeSubscriptionsPreset } from 'graphile-realtime-subscriptions';
|
|
12
|
+
*
|
|
13
|
+
* const preset = {
|
|
14
|
+
* extends: [
|
|
15
|
+
* RealtimeSubscriptionsPreset(),
|
|
16
|
+
* ],
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.RealtimeSubscriptionsPreset = exports.RealtimeSubscriptionsPlugin = exports.createRealtimeSubscriptionsPlugin = void 0;
|
|
22
|
+
var plugin_1 = require("./plugin");
|
|
23
|
+
Object.defineProperty(exports, "createRealtimeSubscriptionsPlugin", { enumerable: true, get: function () { return plugin_1.createRealtimeSubscriptionsPlugin; } });
|
|
24
|
+
Object.defineProperty(exports, "RealtimeSubscriptionsPlugin", { enumerable: true, get: function () { return plugin_1.RealtimeSubscriptionsPlugin; } });
|
|
25
|
+
var preset_1 = require("./preset");
|
|
26
|
+
Object.defineProperty(exports, "RealtimeSubscriptionsPreset", { enumerable: true, get: function () { return preset_1.RealtimeSubscriptionsPreset; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graphile-realtime-subscriptions",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Realtime subscription plugin for PostGraphile v5 — per-table GraphQL subscriptions via LISTEN/NOTIFY",
|
|
5
|
+
"author": "Constructive <developers@constructive.io>",
|
|
6
|
+
"homepage": "https://github.com/constructive-io/constructive",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"module": "esm/index.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"clean": "makage clean",
|
|
13
|
+
"prepack": "npm run build",
|
|
14
|
+
"build": "makage build",
|
|
15
|
+
"build:dev": "makage build --dev",
|
|
16
|
+
"lint": "eslint . --fix",
|
|
17
|
+
"test": "jest --passWithNoTests",
|
|
18
|
+
"test:watch": "jest --watch"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"directory": "dist"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/constructive-io/constructive"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"postgraphile",
|
|
30
|
+
"graphile",
|
|
31
|
+
"constructive",
|
|
32
|
+
"plugin",
|
|
33
|
+
"postgres",
|
|
34
|
+
"graphql",
|
|
35
|
+
"subscriptions",
|
|
36
|
+
"realtime",
|
|
37
|
+
"notify"
|
|
38
|
+
],
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/constructive-io/constructive/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@pgpmjs/logger": "^2.9.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"grafast": "1.0.0",
|
|
47
|
+
"graphile-build": "5.0.0",
|
|
48
|
+
"graphile-build-pg": "5.0.0",
|
|
49
|
+
"graphile-config": "1.0.0",
|
|
50
|
+
"graphile-utils": "5.0.0",
|
|
51
|
+
"graphql": "16.13.0",
|
|
52
|
+
"postgraphile": "5.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^22.19.11",
|
|
56
|
+
"makage": "^0.3.0"
|
|
57
|
+
},
|
|
58
|
+
"gitHead": "c87dafec1ccb2c3cc62f2304584104db108a810b"
|
|
59
|
+
}
|
package/plugin.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
3
|
+
*
|
|
4
|
+
* Discovers tables tagged with @realtime and generates per-table
|
|
5
|
+
* subscription fields (onXxxChanged) that use PostgreSQL LISTEN/NOTIFY
|
|
6
|
+
* for real-time event delivery.
|
|
7
|
+
*
|
|
8
|
+
* Subscription modes (Phase 3a):
|
|
9
|
+
* - Single record: onXxxChanged(id: UUID!) — subscribe to changes on one row
|
|
10
|
+
* - Full collection: onXxxChanged (no args) — subscribe to any change on the table
|
|
11
|
+
*
|
|
12
|
+
* Event flow:
|
|
13
|
+
* 1. A row is inserted/updated/deleted
|
|
14
|
+
* 2. The emit_change trigger fires pg_notify('realtime:{schema}.{table}', TG_OP)
|
|
15
|
+
* 3. PostGraphile's pgSubscriber receives the NOTIFY
|
|
16
|
+
* 4. The subscription re-queries the source table with RLS enforced
|
|
17
|
+
* 5. The client receives { event, row } where row reflects the current state
|
|
18
|
+
*
|
|
19
|
+
* RLS enforcement is automatic — resource.get() queries through the
|
|
20
|
+
* authenticated user's connection with their JWT role applied.
|
|
21
|
+
*/
|
|
22
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
23
|
+
import type { RealtimeSubscriptionsPluginOptions } from './types';
|
|
24
|
+
export declare function createRealtimeSubscriptionsPlugin(_options?: RealtimeSubscriptionsPluginOptions): GraphileConfig.Plugin;
|
|
25
|
+
export { createRealtimeSubscriptionsPlugin as RealtimeSubscriptionsPlugin };
|
package/plugin.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Realtime Subscriptions Plugin for PostGraphile v5
|
|
4
|
+
*
|
|
5
|
+
* Discovers tables tagged with @realtime and generates per-table
|
|
6
|
+
* subscription fields (onXxxChanged) that use PostgreSQL LISTEN/NOTIFY
|
|
7
|
+
* for real-time event delivery.
|
|
8
|
+
*
|
|
9
|
+
* Subscription modes (Phase 3a):
|
|
10
|
+
* - Single record: onXxxChanged(id: UUID!) — subscribe to changes on one row
|
|
11
|
+
* - Full collection: onXxxChanged (no args) — subscribe to any change on the table
|
|
12
|
+
*
|
|
13
|
+
* Event flow:
|
|
14
|
+
* 1. A row is inserted/updated/deleted
|
|
15
|
+
* 2. The emit_change trigger fires pg_notify('realtime:{schema}.{table}', TG_OP)
|
|
16
|
+
* 3. PostGraphile's pgSubscriber receives the NOTIFY
|
|
17
|
+
* 4. The subscription re-queries the source table with RLS enforced
|
|
18
|
+
* 5. The client receives { event, row } where row reflects the current state
|
|
19
|
+
*
|
|
20
|
+
* RLS enforcement is automatic — resource.get() queries through the
|
|
21
|
+
* authenticated user's connection with their JWT role applied.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.createRealtimeSubscriptionsPlugin = createRealtimeSubscriptionsPlugin;
|
|
25
|
+
exports.RealtimeSubscriptionsPlugin = createRealtimeSubscriptionsPlugin;
|
|
26
|
+
const grafast_1 = require("grafast");
|
|
27
|
+
const graphile_utils_1 = require("graphile-utils");
|
|
28
|
+
const logger_1 = require("@pgpmjs/logger");
|
|
29
|
+
const log = new logger_1.Logger('graphile-realtime-subscriptions');
|
|
30
|
+
function discoverRealtimeTables(build) {
|
|
31
|
+
const { pgRegistry } = build.input;
|
|
32
|
+
const resources = pgRegistry.pgResources;
|
|
33
|
+
const result = [];
|
|
34
|
+
for (const [, resource] of Object.entries(resources)) {
|
|
35
|
+
const r = resource;
|
|
36
|
+
const codec = r.codec;
|
|
37
|
+
if (!codec?.attributes)
|
|
38
|
+
continue;
|
|
39
|
+
const tags = codec.extensions?.tags;
|
|
40
|
+
if (!tags?.realtime)
|
|
41
|
+
continue;
|
|
42
|
+
const typeName = build.inflection.tableType(codec);
|
|
43
|
+
const fieldName = `on${typeName}Changed`;
|
|
44
|
+
const payloadTypeName = `${typeName}SubscriptionPayload`;
|
|
45
|
+
const rowFieldName = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
|
46
|
+
const pgSchema = codec.extensions?.pg?.schemaName ?? 'public';
|
|
47
|
+
const pgTable = codec.extensions?.pg?.name ?? codec.name;
|
|
48
|
+
const notifyChannel = `realtime:${pgSchema}.${pgTable}`;
|
|
49
|
+
result.push({
|
|
50
|
+
resource: r,
|
|
51
|
+
typeName,
|
|
52
|
+
fieldName,
|
|
53
|
+
payloadTypeName,
|
|
54
|
+
rowFieldName,
|
|
55
|
+
notifyChannel,
|
|
56
|
+
pgSchema,
|
|
57
|
+
pgTable,
|
|
58
|
+
});
|
|
59
|
+
log.info(`Discovered realtime table: ${pgSchema}.${pgTable} -> ${fieldName}`);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function buildTypeDefs(tables) {
|
|
64
|
+
const subscriptionFields = tables
|
|
65
|
+
.map(({ fieldName, payloadTypeName }) => ` """Subscribe to changes on this table. Pass an id to watch a specific record."""\n ${fieldName}(id: UUID): ${payloadTypeName}`)
|
|
66
|
+
.join('\n');
|
|
67
|
+
const payloadTypes = tables
|
|
68
|
+
.map(({ payloadTypeName, typeName, rowFieldName }) => `"""Payload delivered when a ${typeName} row changes."""\n` +
|
|
69
|
+
`type ${payloadTypeName} {\n` +
|
|
70
|
+
` """The DML operation: INSERT, UPDATE, or DELETE."""\n` +
|
|
71
|
+
` event: String!\n` +
|
|
72
|
+
` """The current state of the row (null for DELETE or if RLS denies access)."""\n` +
|
|
73
|
+
` ${rowFieldName}: ${typeName}\n` +
|
|
74
|
+
`}`)
|
|
75
|
+
.join('\n\n');
|
|
76
|
+
return `extend type Subscription {\n${subscriptionFields}\n}\n\n${payloadTypes}`;
|
|
77
|
+
}
|
|
78
|
+
function buildPlans(tables) {
|
|
79
|
+
const subscriptionPlans = {};
|
|
80
|
+
const allPlans = {};
|
|
81
|
+
for (const { resource, fieldName, payloadTypeName, rowFieldName, notifyChannel } of tables) {
|
|
82
|
+
subscriptionPlans[fieldName] = {
|
|
83
|
+
subscribePlan(_$root, args) {
|
|
84
|
+
const $pgSubscriber = (0, grafast_1.context)().get('pgSubscriber');
|
|
85
|
+
const $topic = (0, grafast_1.constant)(notifyChannel);
|
|
86
|
+
const $id = args.get('id');
|
|
87
|
+
return (0, grafast_1.listen)($pgSubscriber, $topic, ($payload) => {
|
|
88
|
+
return (0, grafast_1.object)({
|
|
89
|
+
event: $payload,
|
|
90
|
+
subscribedId: $id,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
plan($event) {
|
|
95
|
+
return $event;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
allPlans[payloadTypeName] = {
|
|
99
|
+
event($parent) {
|
|
100
|
+
return $parent.get('event');
|
|
101
|
+
},
|
|
102
|
+
[rowFieldName]($parent) {
|
|
103
|
+
const $id = $parent.get('subscribedId');
|
|
104
|
+
return resource.get({ id: $id });
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
allPlans['Subscription'] = subscriptionPlans;
|
|
109
|
+
return allPlans;
|
|
110
|
+
}
|
|
111
|
+
function createRealtimeSubscriptionsPlugin(_options = {}) {
|
|
112
|
+
return (0, graphile_utils_1.extendSchema)((build) => {
|
|
113
|
+
const tables = discoverRealtimeTables(build);
|
|
114
|
+
if (tables.length === 0) {
|
|
115
|
+
log.info('No tables with @realtime tag found — skipping subscription generation');
|
|
116
|
+
return { typeDefs: '', plans: {} };
|
|
117
|
+
}
|
|
118
|
+
log.info(`Generating subscription fields for ${tables.length} realtime table(s)`);
|
|
119
|
+
const typeDefs = buildTypeDefs(tables);
|
|
120
|
+
const plans = buildPlans(tables);
|
|
121
|
+
return { typeDefs, plans };
|
|
122
|
+
}, 'RealtimeSubscriptionsPlugin');
|
|
123
|
+
}
|
package/preset.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Realtime Subscriptions Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset for including realtime subscription support
|
|
5
|
+
* in PostGraphile. Wraps the RealtimeSubscriptionsPlugin with options.
|
|
6
|
+
*/
|
|
7
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
8
|
+
import type { RealtimeSubscriptionsPluginOptions } from './types';
|
|
9
|
+
export declare function RealtimeSubscriptionsPreset(options?: RealtimeSubscriptionsPluginOptions): GraphileConfig.Preset;
|
|
10
|
+
export default RealtimeSubscriptionsPreset;
|
package/preset.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PostGraphile v5 Realtime Subscriptions Preset
|
|
4
|
+
*
|
|
5
|
+
* Provides a convenient preset for including realtime subscription support
|
|
6
|
+
* in PostGraphile. Wraps the RealtimeSubscriptionsPlugin with options.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.RealtimeSubscriptionsPreset = RealtimeSubscriptionsPreset;
|
|
10
|
+
const plugin_1 = require("./plugin");
|
|
11
|
+
function RealtimeSubscriptionsPreset(options = {}) {
|
|
12
|
+
return {
|
|
13
|
+
plugins: [(0, plugin_1.createRealtimeSubscriptionsPlugin)(options)],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
exports.default = RealtimeSubscriptionsPreset;
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the Realtime Subscriptions Plugin.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3a supports single-record and full-collection subscription modes
|
|
5
|
+
* with no additional configuration required. Future phases will add
|
|
6
|
+
* options for poll intervals, batch sizes, and throttling.
|
|
7
|
+
*/
|
|
8
|
+
export interface RealtimeSubscriptionsPluginOptions {
|
|
9
|
+
}
|
package/types.js
ADDED