graphile-history 1.1.1

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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "graphile-history",
3
+ "version": "1.1.1",
4
+ "description": "PostGraphile v5 history plugin — per-row version history, point-in-time reads, and restore mutations for @history tables",
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 --forceExit",
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
+ "bugs": {
29
+ "url": "https://github.com/constructive-io/constructive/issues"
30
+ },
31
+ "peerDependencies": {
32
+ "@dataplan/pg": "1.0.3",
33
+ "grafast": "1.0.2",
34
+ "graphile-build": "5.0.2",
35
+ "graphile-build-pg": "5.0.2",
36
+ "graphile-config": "1.0.1",
37
+ "graphql": "16.13.0",
38
+ "pg-sql2": "5.0.1",
39
+ "postgraphile": "5.0.3"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.19.11",
43
+ "@types/pg": "^8.20.0",
44
+ "graphile-test": "^5.9.6",
45
+ "makage": "^0.3.0",
46
+ "pg": "^8.21.0",
47
+ "pgsql-test": "^5.8.6"
48
+ },
49
+ "keywords": [
50
+ "postgraphile",
51
+ "graphile",
52
+ "constructive",
53
+ "plugin",
54
+ "postgres",
55
+ "graphql",
56
+ "history",
57
+ "audit",
58
+ "versioning",
59
+ "temporal",
60
+ "restore"
61
+ ],
62
+ "gitHead": "a430897efa1488ec24fc636f0d5023207bba7ba7"
63
+ }
package/plugin.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * PostGraphile v5 History Plugin
3
+ *
4
+ * Discovers tables tagged with `@history` (companion `<table>_history` tables
5
+ * produced by the constructive-db `DataHistory` module) and augments the schema
6
+ * with:
7
+ * - a `history` connection field on each source row type — the row's full
8
+ * version stream ordered by `recorded_at DESC`;
9
+ * - a `versionAt(at: Datetime!)` field returning the version that was current
10
+ * at a given point in time;
11
+ * - a `restore<Table>Version` root mutation that rewrites the live row from a
12
+ * historical version (optionally re-inserting a deleted row).
13
+ *
14
+ * All reads and writes go through the request's `withPgClient` + `pgSettings`,
15
+ * so row-level security and mutation policies are enforced exactly as they are
16
+ * for any other operation. Because restores write through the source table, the
17
+ * source `DataHistory` trigger records the restore itself as a new version.
18
+ *
19
+ * Smart tag format (stamped by `metaschema_generators.data_history`):
20
+ * @history { "history_table": "posts_history", ... }
21
+ * The tag value may also be a bare string naming the history table, or simply a
22
+ * truthy marker (in which case the history table is derived by suffix).
23
+ */
24
+ import 'graphile-build';
25
+ import 'graphile-build-pg';
26
+ import type { GraphileConfig } from 'graphile-config';
27
+ import type { HistoryPluginOptions } from './types';
28
+ declare global {
29
+ namespace GraphileConfig {
30
+ interface Plugins {
31
+ HistoryPlugin: true;
32
+ }
33
+ }
34
+ }
35
+ export declare function createHistoryPlugin(options?: HistoryPluginOptions): GraphileConfig.Plugin;
36
+ export declare const HistoryPlugin: GraphileConfig.Plugin;
package/plugin.js ADDED
@@ -0,0 +1,516 @@
1
+ "use strict";
2
+ /**
3
+ * PostGraphile v5 History Plugin
4
+ *
5
+ * Discovers tables tagged with `@history` (companion `<table>_history` tables
6
+ * produced by the constructive-db `DataHistory` module) and augments the schema
7
+ * with:
8
+ * - a `history` connection field on each source row type — the row's full
9
+ * version stream ordered by `recorded_at DESC`;
10
+ * - a `versionAt(at: Datetime!)` field returning the version that was current
11
+ * at a given point in time;
12
+ * - a `restore<Table>Version` root mutation that rewrites the live row from a
13
+ * historical version (optionally re-inserting a deleted row).
14
+ *
15
+ * All reads and writes go through the request's `withPgClient` + `pgSettings`,
16
+ * so row-level security and mutation policies are enforced exactly as they are
17
+ * for any other operation. Because restores write through the source table, the
18
+ * source `DataHistory` trigger records the restore itself as a new version.
19
+ *
20
+ * Smart tag format (stamped by `metaschema_generators.data_history`):
21
+ * @history { "history_table": "posts_history", ... }
22
+ * The tag value may also be a bare string naming the history table, or simply a
23
+ * truthy marker (in which case the history table is derived by suffix).
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.HistoryPlugin = void 0;
27
+ exports.createHistoryPlugin = createHistoryPlugin;
28
+ require("graphile-build");
29
+ require("graphile-build-pg");
30
+ const grafast_1 = require("grafast");
31
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
32
+ function readHistoryTag(codec) {
33
+ const tags = codec.extensions?.tags;
34
+ if (!tags)
35
+ return false;
36
+ const val = tags.history;
37
+ if (val === undefined || val === null || val === false)
38
+ return false;
39
+ if (typeof val === 'string') {
40
+ // Tag may be a bare table name or a JSON object encoded as a string.
41
+ const trimmed = val.trim();
42
+ if (trimmed.startsWith('{')) {
43
+ try {
44
+ const parsed = JSON.parse(trimmed);
45
+ return parsed?.history_table ?? true;
46
+ }
47
+ catch {
48
+ return trimmed.length > 0 ? trimmed : true;
49
+ }
50
+ }
51
+ return trimmed.length > 0 ? trimmed : true;
52
+ }
53
+ if (typeof val === 'object') {
54
+ return val.history_table ?? true;
55
+ }
56
+ return true;
57
+ }
58
+ function resolvePgTypeName(codec) {
59
+ const name = codec?.name;
60
+ if (name)
61
+ return name;
62
+ return 'text';
63
+ }
64
+ function pgTypeToGraphQLType(build, pgType) {
65
+ const { graphql: { GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean } } = build;
66
+ const byName = (n) => build.getTypeByName(n);
67
+ switch (pgType) {
68
+ case 'uuid':
69
+ return byName('UUID') ?? GraphQLString;
70
+ case 'int2':
71
+ case 'int4':
72
+ return GraphQLInt;
73
+ case 'int8':
74
+ return byName('BigInt') ?? GraphQLString;
75
+ case 'bool':
76
+ return GraphQLBoolean;
77
+ case 'float4':
78
+ case 'float8':
79
+ return GraphQLFloat;
80
+ case 'numeric':
81
+ return byName('BigFloat') ?? GraphQLString;
82
+ case 'timestamptz':
83
+ case 'timestamp':
84
+ case 'date':
85
+ case 'time':
86
+ case 'timetz':
87
+ return byName('Datetime') ?? GraphQLString;
88
+ case 'json':
89
+ case 'jsonb':
90
+ return byName('JSON') ?? GraphQLString;
91
+ default:
92
+ return GraphQLString;
93
+ }
94
+ }
95
+ function quoteIdent(ident) {
96
+ return `"${ident.replace(/"/g, '""')}"`;
97
+ }
98
+ // ─── Plugin Factory ──────────────────────────────────────────────────────────
99
+ function createHistoryPlugin(options = {}) {
100
+ const { historySuffix = '_history', recordedAtColumn = 'recorded_at', operationColumn = 'history_op', immutableColumns = ['created_at', 'updated_at'] } = options;
101
+ // Closure state shared across hooks
102
+ let registry = {};
103
+ const versionTypeCache = {};
104
+ function getVersionType(build, info) {
105
+ const { graphql: { GraphQLObjectType, GraphQLString } } = build;
106
+ const typeName = `${info.sourceTypeName}HistoryVersion`;
107
+ if (versionTypeCache[typeName])
108
+ return versionTypeCache[typeName];
109
+ const fieldsConfig = {};
110
+ for (const col of info.copyColumns) {
111
+ fieldsConfig[col.gqlName] = { type: pgTypeToGraphQLType(build, col.pgType) };
112
+ }
113
+ fieldsConfig.recordedAt = { type: build.getTypeByName('Datetime') ?? GraphQLString };
114
+ fieldsConfig.historyOp = { type: GraphQLString };
115
+ const type = new GraphQLObjectType({
116
+ name: typeName,
117
+ description: `A single historical version of a ${info.sourceTypeName} row.`,
118
+ fields: fieldsConfig
119
+ });
120
+ versionTypeCache[typeName] = type;
121
+ return type;
122
+ }
123
+ // Map a raw history row (physical column keys) to the GraphQL version shape.
124
+ function mapVersionRow(info, row) {
125
+ if (!row)
126
+ return null;
127
+ const out = {};
128
+ for (const col of info.copyColumns) {
129
+ out[col.gqlName] = row[col.column] ?? null;
130
+ }
131
+ out.recordedAt = row[info.recordedAtColumn] ?? null;
132
+ out.historyOp = row[info.operationColumn] ?? null;
133
+ return out;
134
+ }
135
+ function selectColumnsSql(info) {
136
+ const cols = info.copyColumns.map((c) => quoteIdent(c.column));
137
+ cols.push(quoteIdent(info.recordedAtColumn));
138
+ cols.push(quoteIdent(info.operationColumn));
139
+ return cols.join(', ');
140
+ }
141
+ return {
142
+ name: 'HistoryPlugin',
143
+ version: '1.0.0',
144
+ schema: {
145
+ hooks: {
146
+ init: {
147
+ callback(_, build) {
148
+ registry = {};
149
+ const pgResources = build.input.pgRegistry.pgResources;
150
+ for (const [, resource] of Object.entries(pgResources)) {
151
+ const r = resource;
152
+ const codec = r.codec;
153
+ if (!codec?.attributes)
154
+ continue;
155
+ const tagValue = readHistoryTag(codec);
156
+ if (!tagValue)
157
+ continue;
158
+ const schemaName = r.extensions?.pg?.schemaName ?? codec.extensions?.pg?.schemaName ?? 'public';
159
+ const sourceSqlName = codec.extensions?.pg?.name ?? r.name ?? codec.name;
160
+ // Resolve history table name: explicit tag value, else suffix convention.
161
+ const historyTableName = typeof tagValue === 'string' ? tagValue : `${sourceSqlName}${historySuffix}`;
162
+ // Locate the history resource/codec by SQL name in the same schema.
163
+ let historyCodec = null;
164
+ for (const [, hres] of Object.entries(pgResources)) {
165
+ const hr = hres;
166
+ const hcodec = hr.codec;
167
+ if (!hcodec?.attributes)
168
+ continue;
169
+ const hSchema = hr.extensions?.pg?.schemaName ?? hcodec.extensions?.pg?.schemaName ?? 'public';
170
+ const hName = hcodec.extensions?.pg?.name ?? hr.name ?? hcodec.name;
171
+ if (hName === historyTableName && hSchema === schemaName) {
172
+ historyCodec = hcodec;
173
+ break;
174
+ }
175
+ }
176
+ if (!historyCodec)
177
+ continue;
178
+ // The history table must carry the reserved columns.
179
+ if (!historyCodec.attributes[recordedAtColumn] || !historyCodec.attributes[operationColumn]) {
180
+ continue;
181
+ }
182
+ // Primary key columns from the source resource's uniques.
183
+ const uniques = r.uniques;
184
+ const pk = uniques?.find((u) => u.isPrimary);
185
+ if (!pk || pk.attributes.length === 0)
186
+ continue;
187
+ const pkColumns = pk.attributes.map((attr) => ({
188
+ column: attr,
189
+ gqlName: build.inflection.camelCase(attr),
190
+ pgType: resolvePgTypeName(codec.attributes[attr]?.codec)
191
+ }));
192
+ // Copied columns = history columns present on the source, minus reserved.
193
+ const copyColumns = [];
194
+ for (const [attrName, attr] of Object.entries(historyCodec.attributes)) {
195
+ if (attrName === recordedAtColumn || attrName === operationColumn)
196
+ continue;
197
+ if (!codec.attributes[attrName])
198
+ continue;
199
+ copyColumns.push({
200
+ column: attrName,
201
+ gqlName: build.inflection.camelCase(attrName),
202
+ pgType: resolvePgTypeName(attr.codec)
203
+ });
204
+ }
205
+ if (copyColumns.length === 0)
206
+ continue;
207
+ const sourceTypeName = build.inflection.tableType(codec);
208
+ registry[sourceTypeName] = {
209
+ sourceTable: sourceSqlName,
210
+ historyTable: historyTableName,
211
+ schemaName,
212
+ pkColumns,
213
+ recordedAtColumn,
214
+ operationColumn,
215
+ copyColumns,
216
+ sourceTypeName
217
+ };
218
+ }
219
+ return _;
220
+ }
221
+ },
222
+ GraphQLObjectType_fields(fields, build, context) {
223
+ const { graphql: { GraphQLNonNull, GraphQLList, GraphQLBoolean, GraphQLInputObjectType, GraphQLObjectType, GraphQLString } } = build;
224
+ const { scope } = context;
225
+ // ── Root mutation: restore<Table>Version ────────────────────────────
226
+ if (scope.isRootMutation) {
227
+ const mutationFields = {};
228
+ for (const info of Object.values(registry)) {
229
+ const versionType = getVersionType(build, info);
230
+ const inputFieldsConfig = {};
231
+ for (const pk of info.pkColumns) {
232
+ inputFieldsConfig[pk.gqlName] = {
233
+ type: new GraphQLNonNull(pgTypeToGraphQLType(build, pk.pgType))
234
+ };
235
+ }
236
+ inputFieldsConfig.recordedAt = {
237
+ type: new GraphQLNonNull(build.getTypeByName('Datetime') ?? GraphQLString)
238
+ };
239
+ inputFieldsConfig.reinsert = { type: GraphQLBoolean };
240
+ const inputTypeName = `Restore${info.sourceTypeName}VersionInput`;
241
+ const inputType = new GraphQLInputObjectType({
242
+ name: inputTypeName,
243
+ fields: inputFieldsConfig
244
+ });
245
+ const payloadTypeName = `Restore${info.sourceTypeName}VersionPayload`;
246
+ const payloadType = new GraphQLObjectType({
247
+ name: payloadTypeName,
248
+ fields: {
249
+ version: { type: versionType },
250
+ restored: { type: versionType }
251
+ }
252
+ });
253
+ const fieldName = `restore${info.sourceTypeName}Version`;
254
+ mutationFields[fieldName] = {
255
+ type: payloadType,
256
+ args: {
257
+ input: { type: new GraphQLNonNull(inputType) }
258
+ },
259
+ plan(_$root, fieldArgs) {
260
+ const $input = fieldArgs.getRaw('input');
261
+ const $withPgClient = (0, grafast_1.context)().get('withPgClient');
262
+ const $pgSettings = (0, grafast_1.context)().get('pgSettings');
263
+ const $combined = (0, grafast_1.object)({
264
+ input: $input,
265
+ withPgClient: $withPgClient,
266
+ pgSettings: $pgSettings
267
+ });
268
+ return (0, grafast_1.lambda)($combined, async (vals) => {
269
+ const { input, withPgClient, pgSettings } = vals;
270
+ if (!withPgClient || !input)
271
+ return null;
272
+ const schema = quoteIdent(info.schemaName);
273
+ const source = `${schema}.${quoteIdent(info.sourceTable)}`;
274
+ const history = `${schema}.${quoteIdent(info.historyTable)}`;
275
+ // 1. Locate the requested version (point-in-time: latest at/<= recordedAt).
276
+ const params = [];
277
+ const pkPredicates = info.pkColumns.map((pk) => {
278
+ params.push(input[pk.gqlName]);
279
+ return `${quoteIdent(pk.column)} = $${params.length}`;
280
+ });
281
+ params.push(input.recordedAt);
282
+ const atParam = `$${params.length}`;
283
+ const versionSql = `SELECT ${selectColumnsSql(info)}
284
+ FROM ${history}
285
+ WHERE ${pkPredicates.join(' AND ')}
286
+ AND ${quoteIdent(info.recordedAtColumn)} <= ${atParam}
287
+ ORDER BY ${quoteIdent(info.recordedAtColumn)} DESC
288
+ LIMIT 1`;
289
+ return withPgClient(pgSettings, async (pgClient) => {
290
+ const versionRes = await pgClient.query({ text: versionSql, values: params });
291
+ const versionRow = versionRes.rows[0] ?? null;
292
+ if (!versionRow) {
293
+ return { version: null, restored: null };
294
+ }
295
+ // 2. Columns we may write back: copied columns minus PK / immutable.
296
+ const pkNames = new Set(info.pkColumns.map((c) => c.column));
297
+ const immutable = new Set(immutableColumns);
298
+ const writable = info.copyColumns.filter((c) => !pkNames.has(c.column) && !immutable.has(c.column));
299
+ // 3. Does the live row still exist?
300
+ const existsParams = [];
301
+ const existsPreds = info.pkColumns.map((pk) => {
302
+ existsParams.push(input[pk.gqlName]);
303
+ return `${quoteIdent(pk.column)} = $${existsParams.length}`;
304
+ });
305
+ const existsRes = await pgClient.query({
306
+ text: `SELECT 1 FROM ${source} WHERE ${existsPreds.join(' AND ')} LIMIT 1`,
307
+ values: existsParams
308
+ });
309
+ const rowExists = existsRes.rows.length > 0;
310
+ if (rowExists) {
311
+ if (writable.length > 0) {
312
+ const setParams = [];
313
+ const setClauses = writable.map((c) => {
314
+ setParams.push(versionRow[c.column] ?? null);
315
+ return `${quoteIdent(c.column)} = $${setParams.length}`;
316
+ });
317
+ const wherePreds = info.pkColumns.map((pk) => {
318
+ setParams.push(input[pk.gqlName]);
319
+ return `${quoteIdent(pk.column)} = $${setParams.length}`;
320
+ });
321
+ await pgClient.query({
322
+ text: `UPDATE ${source} SET ${setClauses.join(', ')} WHERE ${wherePreds.join(' AND ')}`,
323
+ values: setParams
324
+ });
325
+ }
326
+ }
327
+ else if (input.reinsert) {
328
+ const insertCols = info.copyColumns.filter((c) => !immutable.has(c.column));
329
+ const insParams = [];
330
+ const placeholders = insertCols.map((c) => {
331
+ insParams.push(versionRow[c.column] ?? null);
332
+ return `$${insParams.length}`;
333
+ });
334
+ await pgClient.query({
335
+ text: `INSERT INTO ${source} (${insertCols
336
+ .map((c) => quoteIdent(c.column))
337
+ .join(', ')}) VALUES (${placeholders.join(', ')})`,
338
+ values: insParams
339
+ });
340
+ }
341
+ else {
342
+ // Row is deleted and reinsert not requested — nothing to restore.
343
+ return { version: mapVersionRow(info, versionRow), restored: null };
344
+ }
345
+ // 4. Re-read the current source row to report the restored state.
346
+ const curParams = [];
347
+ const curPreds = info.pkColumns.map((pk) => {
348
+ curParams.push(input[pk.gqlName]);
349
+ return `${quoteIdent(pk.column)} = $${curParams.length}`;
350
+ });
351
+ const curCols = info.copyColumns.map((c) => quoteIdent(c.column)).join(', ');
352
+ const curRes = await pgClient.query({
353
+ text: `SELECT ${curCols} FROM ${source} WHERE ${curPreds.join(' AND ')} LIMIT 1`,
354
+ values: curParams
355
+ });
356
+ const curRow = curRes.rows[0] ?? null;
357
+ const restored = curRow
358
+ ? (() => {
359
+ const out = {};
360
+ for (const c of info.copyColumns)
361
+ out[c.gqlName] = curRow[c.column] ?? null;
362
+ out.recordedAt = null;
363
+ out.historyOp = null;
364
+ return out;
365
+ })()
366
+ : null;
367
+ return { version: mapVersionRow(info, versionRow), restored };
368
+ });
369
+ });
370
+ }
371
+ };
372
+ }
373
+ if (Object.keys(mutationFields).length === 0)
374
+ return fields;
375
+ return build.extend(fields, mutationFields, 'Adding history restore mutations');
376
+ }
377
+ // ── Source row type: history + versionAt ────────────────────────────
378
+ if (!scope.pgCodec || !scope.isPgClassType)
379
+ return fields;
380
+ const codec = scope.pgCodec;
381
+ const sourceTypeName = build.inflection.tableType(codec);
382
+ const info = registry[sourceTypeName];
383
+ if (!info)
384
+ return fields;
385
+ const versionType = getVersionType(build, info);
386
+ const historyQualified = `${quoteIdent(info.schemaName)}.${quoteIdent(info.historyTable)}`;
387
+ const orderClause = `ORDER BY ${quoteIdent(info.recordedAtColumn)} DESC`;
388
+ function planParentPk($parent) {
389
+ const pkSteps = {};
390
+ for (const pk of info.pkColumns) {
391
+ pkSteps[`pk_${pk.column}`] = $parent.get(pk.column);
392
+ }
393
+ return pkSteps;
394
+ }
395
+ return build.extend(fields, {
396
+ history: {
397
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(versionType))),
398
+ description: `Full version history of this ${info.sourceTypeName}, newest first.`,
399
+ plan($parent) {
400
+ const $withPgClient = (0, grafast_1.context)().get('withPgClient');
401
+ const $pgSettings = (0, grafast_1.context)().get('pgSettings');
402
+ const $combined = (0, grafast_1.object)({
403
+ withPgClient: $withPgClient,
404
+ pgSettings: $pgSettings,
405
+ ...planParentPk($parent)
406
+ });
407
+ return (0, grafast_1.lambda)($combined, async (vals) => {
408
+ const { withPgClient, pgSettings } = vals;
409
+ if (!withPgClient)
410
+ return [];
411
+ const params = [];
412
+ const preds = info.pkColumns.map((pk) => {
413
+ params.push(vals[`pk_${pk.column}`]);
414
+ return `${quoteIdent(pk.column)} = $${params.length}`;
415
+ });
416
+ const sql = `SELECT ${selectColumnsSql(info)} FROM ${historyQualified}
417
+ WHERE ${preds.join(' AND ')} ${orderClause}`;
418
+ const rows = await withPgClient(pgSettings, async (pgClient) => {
419
+ const res = await pgClient.query({ text: sql, values: params });
420
+ return res.rows;
421
+ });
422
+ return rows.map((row) => mapVersionRow(info, row));
423
+ });
424
+ }
425
+ },
426
+ versionAt: {
427
+ type: versionType,
428
+ description: `The version of this ${info.sourceTypeName} that was current at the given time.`,
429
+ args: {
430
+ at: { type: new GraphQLNonNull(build.getTypeByName('Datetime') ?? GraphQLString) }
431
+ },
432
+ plan($parent, fieldArgs) {
433
+ const $at = fieldArgs.getRaw('at');
434
+ const $withPgClient = (0, grafast_1.context)().get('withPgClient');
435
+ const $pgSettings = (0, grafast_1.context)().get('pgSettings');
436
+ const $combined = (0, grafast_1.object)({
437
+ at: $at,
438
+ withPgClient: $withPgClient,
439
+ pgSettings: $pgSettings,
440
+ ...planParentPk($parent)
441
+ });
442
+ return (0, grafast_1.lambda)($combined, async (vals) => {
443
+ const { at, withPgClient, pgSettings } = vals;
444
+ if (!withPgClient)
445
+ return null;
446
+ const params = [];
447
+ const preds = info.pkColumns.map((pk) => {
448
+ params.push(vals[`pk_${pk.column}`]);
449
+ return `${quoteIdent(pk.column)} = $${params.length}`;
450
+ });
451
+ params.push(at);
452
+ const atParam = `$${params.length}`;
453
+ const sql = `SELECT ${selectColumnsSql(info)} FROM ${historyQualified}
454
+ WHERE ${preds.join(' AND ')}
455
+ AND ${quoteIdent(info.recordedAtColumn)} <= ${atParam}
456
+ ${orderClause} LIMIT 1`;
457
+ const row = await withPgClient(pgSettings, async (pgClient) => {
458
+ const res = await pgClient.query({ text: sql, values: params });
459
+ return res.rows[0] ?? null;
460
+ });
461
+ return mapVersionRow(info, row);
462
+ });
463
+ }
464
+ },
465
+ versionsBetween: {
466
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(versionType))),
467
+ description: `Versions of this ${info.sourceTypeName} recorded within the given time window (inclusive), newest first.`,
468
+ args: {
469
+ from: { type: new GraphQLNonNull(build.getTypeByName('Datetime') ?? GraphQLString) },
470
+ to: { type: new GraphQLNonNull(build.getTypeByName('Datetime') ?? GraphQLString) }
471
+ },
472
+ plan($parent, fieldArgs) {
473
+ const $from = fieldArgs.getRaw('from');
474
+ const $to = fieldArgs.getRaw('to');
475
+ const $withPgClient = (0, grafast_1.context)().get('withPgClient');
476
+ const $pgSettings = (0, grafast_1.context)().get('pgSettings');
477
+ const $combined = (0, grafast_1.object)({
478
+ from: $from,
479
+ to: $to,
480
+ withPgClient: $withPgClient,
481
+ pgSettings: $pgSettings,
482
+ ...planParentPk($parent)
483
+ });
484
+ return (0, grafast_1.lambda)($combined, async (vals) => {
485
+ const { from, to, withPgClient, pgSettings } = vals;
486
+ if (!withPgClient)
487
+ return [];
488
+ const params = [];
489
+ const preds = info.pkColumns.map((pk) => {
490
+ params.push(vals[`pk_${pk.column}`]);
491
+ return `${quoteIdent(pk.column)} = $${params.length}`;
492
+ });
493
+ params.push(from);
494
+ const fromParam = `$${params.length}`;
495
+ params.push(to);
496
+ const toParam = `$${params.length}`;
497
+ const sql = `SELECT ${selectColumnsSql(info)} FROM ${historyQualified}
498
+ WHERE ${preds.join(' AND ')}
499
+ AND ${quoteIdent(info.recordedAtColumn)} >= ${fromParam}
500
+ AND ${quoteIdent(info.recordedAtColumn)} <= ${toParam}
501
+ ${orderClause}`;
502
+ const rows = await withPgClient(pgSettings, async (pgClient) => {
503
+ const res = await pgClient.query({ text: sql, values: params });
504
+ return res.rows;
505
+ });
506
+ return rows.map((row) => mapVersionRow(info, row));
507
+ });
508
+ }
509
+ }
510
+ }, `Adding history fields to ${info.sourceTypeName}`);
511
+ }
512
+ }
513
+ }
514
+ };
515
+ }
516
+ exports.HistoryPlugin = createHistoryPlugin();
package/preset.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PostGraphile v5 History Preset
3
+ *
4
+ * Convenience preset that bundles the history plugin with configurable options.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { HistoryPreset } from 'graphile-history';
9
+ *
10
+ * const preset = {
11
+ * extends: [
12
+ * HistoryPreset(),
13
+ * ],
14
+ * };
15
+ * ```
16
+ */
17
+ import type { GraphileConfig } from 'graphile-config';
18
+ import type { HistoryPluginOptions } from './types';
19
+ export declare function HistoryPreset(options?: HistoryPluginOptions): GraphileConfig.Preset;
20
+ export default HistoryPreset;
package/preset.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * PostGraphile v5 History Preset
4
+ *
5
+ * Convenience preset that bundles the history plugin with configurable options.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { HistoryPreset } from 'graphile-history';
10
+ *
11
+ * const preset = {
12
+ * extends: [
13
+ * HistoryPreset(),
14
+ * ],
15
+ * };
16
+ * ```
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.HistoryPreset = HistoryPreset;
20
+ const plugin_1 = require("./plugin");
21
+ function HistoryPreset(options = {}) {
22
+ return {
23
+ plugins: [(0, plugin_1.createHistoryPlugin)(options)]
24
+ };
25
+ }
26
+ exports.default = HistoryPreset;