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