@venturekit/data 0.0.31 → 0.0.33
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/dist/files/index.d.ts +9 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +8 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/postgres.d.ts +150 -0
- package/dist/files/postgres.d.ts.map +1 -0
- package/dist/files/postgres.js +194 -0
- package/dist/files/postgres.js.map +1 -0
- package/dist/idempotency/index.d.ts +9 -0
- package/dist/idempotency/index.d.ts.map +1 -0
- package/dist/idempotency/index.js +8 -0
- package/dist/idempotency/index.js.map +1 -0
- package/dist/idempotency/postgres.d.ts +107 -0
- package/dist/idempotency/postgres.d.ts.map +1 -0
- package/dist/idempotency/postgres.js +145 -0
- package/dist/idempotency/postgres.js.map +1 -0
- package/dist/internal/identifier.d.ts +16 -0
- package/dist/internal/identifier.d.ts.map +1 -0
- package/dist/internal/identifier.js +23 -0
- package/dist/internal/identifier.js.map +1 -0
- package/dist/jobs/index.d.ts +9 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +8 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/postgres.d.ts +197 -0
- package/dist/jobs/postgres.d.ts.map +1 -0
- package/dist/jobs/postgres.js +270 -0
- package/dist/jobs/postgres.js.map +1 -0
- package/dist/outbox/index.d.ts +9 -0
- package/dist/outbox/index.d.ts.map +1 -0
- package/dist/outbox/index.js +8 -0
- package/dist/outbox/index.js.map +1 -0
- package/dist/outbox/postgres.d.ts +124 -0
- package/dist/outbox/postgres.d.ts.map +1 -0
- package/dist/outbox/postgres.js +177 -0
- package/dist/outbox/postgres.js.map +1 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js.map +1 -1
- package/dist/query/secret.d.ts.map +1 -1
- package/dist/query/secret.js +1 -1
- package/dist/query/secret.js.map +1 -1
- package/package.json +23 -3
- package/src/sql/0000_vk_data_foundation.sql +305 -0
- package/src/sql/vk_data_001_idempotency.sql +48 -0
- package/src/sql/vk_data_002_outbox.sql +99 -0
- package/src/sql/vk_data_003_jobs.sql +114 -0
- package/src/sql/vk_data_004_file_object.sql +112 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — uploaded-object metadata.
|
|
3
|
+
*
|
|
4
|
+
* The record of what was uploaded; `@venturekit/storage` holds the bytes. See
|
|
5
|
+
* `./postgres.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export { recordFileObject, getFileObject, listFileObjects, findFileObjectByChecksum, archiveFileObject, sweepArchivedFiles, DEFAULT_FILE_TABLE, } from './postgres.js';
|
|
8
|
+
export type { FileObject, RecordFileObjectInput, ListFileObjectsOptions, SweepArchivedFilesOptions, SweepResult, ObjectDeleter, FileTableOption, FileQuerierOption, } from './postgres.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,yBAAyB,EACzB,WAAW,EACX,aAAa,EACb,eAAe,EACf,iBAAiB,GAClB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — uploaded-object metadata.
|
|
3
|
+
*
|
|
4
|
+
* The record of what was uploaded; `@venturekit/storage` holds the bytes. See
|
|
5
|
+
* `./postgres.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export { recordFileObject, getFileObject, listFileObjects, findFileObjectByChecksum, archiveFileObject, sweepArchivedFiles, DEFAULT_FILE_TABLE, } from './postgres.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — uploaded-object metadata.
|
|
3
|
+
*
|
|
4
|
+
* `@venturekit/storage` is complete on the object itself: put, get, head,
|
|
5
|
+
* presign, copy, image optimisation. What a bucket cannot tell you is anything a
|
|
6
|
+
* product needs to know about an upload — which tenant owns it, what it is for,
|
|
7
|
+
* who put it there, whether these exact bytes are already stored, and when it
|
|
8
|
+
* may be deleted. `ListObjectsV2` plus a key-naming convention is the usual
|
|
9
|
+
* substitute and it fails at the first "every document for this tenant, newest
|
|
10
|
+
* first".
|
|
11
|
+
*
|
|
12
|
+
* This module is the record; that package is the bytes. They are deliberately
|
|
13
|
+
* not coupled: `@venturekit/storage` has no `pg` dependency and this has no
|
|
14
|
+
* `@aws-sdk/client-s3` one, so {@link sweepArchivedFiles} takes the object
|
|
15
|
+
* deletion as a port and the consumer wires the two together:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* await sweepArchivedFiles({
|
|
19
|
+
* deleteObjects: (keys) => storage.deleteMany(keys).then(() => undefined),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import type { Querier } from '../query/index.js';
|
|
24
|
+
/** Default table name, created by `vk_data_004_file_object.sql`. */
|
|
25
|
+
export declare const DEFAULT_FILE_TABLE = "vk_file_object";
|
|
26
|
+
export interface FileObject {
|
|
27
|
+
id: string;
|
|
28
|
+
tenantId: string | null;
|
|
29
|
+
purpose: string;
|
|
30
|
+
bucket: string;
|
|
31
|
+
objectKey: string;
|
|
32
|
+
contentType: string;
|
|
33
|
+
sizeBytes: number;
|
|
34
|
+
checksum: string | null;
|
|
35
|
+
originalName: string | null;
|
|
36
|
+
uploadedBy: string | null;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
archivedAt: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface RecordFileObjectInput {
|
|
41
|
+
/** What the file is for. Decides retention and access in your product. */
|
|
42
|
+
purpose: string;
|
|
43
|
+
bucket: string;
|
|
44
|
+
objectKey: string;
|
|
45
|
+
contentType: string;
|
|
46
|
+
sizeBytes: number;
|
|
47
|
+
/** `null` for platform-wide artefacts. */
|
|
48
|
+
tenantId?: string | null;
|
|
49
|
+
/** SHA-256 hex, so a re-upload of identical bytes can be recognised. */
|
|
50
|
+
checksum?: string | null;
|
|
51
|
+
/** The name the user's file had — never the storage key. */
|
|
52
|
+
originalName?: string | null;
|
|
53
|
+
uploadedBy?: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface FileTableOption {
|
|
56
|
+
/** Table name. Validated as an identifier, since it is interpolated. */
|
|
57
|
+
tableName?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface FileQuerierOption extends FileTableOption {
|
|
60
|
+
/** Runs the statements. Defaults to the pooled `query`. */
|
|
61
|
+
querier?: Querier;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Record an uploaded object.
|
|
65
|
+
*
|
|
66
|
+
* Call it in the transaction that also stores whatever references the file, so a
|
|
67
|
+
* rolled-back request cannot leave a row pointing at an object nothing uses —
|
|
68
|
+
* which is why `querier` is a required first argument here as in
|
|
69
|
+
* `appendToOutbox` and `enqueueJob`.
|
|
70
|
+
*
|
|
71
|
+
* **Record after the upload succeeds, not before.** The reverse ordering (row,
|
|
72
|
+
* then upload) produces a row whose object never arrived, and nothing
|
|
73
|
+
* distinguishes that from an object deleted later.
|
|
74
|
+
*/
|
|
75
|
+
export declare function recordFileObject(querier: Querier, input: RecordFileObjectInput, options?: FileTableOption): Promise<string>;
|
|
76
|
+
/** One file by id. Archived rows are returned — callers may need to show them. */
|
|
77
|
+
export declare function getFileObject(id: string, options?: FileQuerierOption): Promise<FileObject | null>;
|
|
78
|
+
export interface ListFileObjectsOptions extends FileQuerierOption {
|
|
79
|
+
purpose?: string;
|
|
80
|
+
limit?: number;
|
|
81
|
+
/** Include archived rows. Default `false`. */
|
|
82
|
+
includeArchived?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Confine to the request's tenant scope (`vk_tenant_scope()`). Default
|
|
85
|
+
* `true`: this table carries no RLS policy by default, because the installer
|
|
86
|
+
* needs the consumer's role name, so the predicate is applied here instead.
|
|
87
|
+
*/
|
|
88
|
+
tenantScoped?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Live files for the current scope, newest first. */
|
|
91
|
+
export declare function listFileObjects(options?: ListFileObjectsOptions): Promise<FileObject[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Find a live file with these exact bytes, for de-duplication on re-upload.
|
|
94
|
+
*
|
|
95
|
+
* Scoped to the tenant by default and deliberately so: matching a checksum
|
|
96
|
+
* across tenants would let one tenant's upload resolve to another's object,
|
|
97
|
+
* which is a cross-tenant read dressed up as an optimisation.
|
|
98
|
+
*/
|
|
99
|
+
export declare function findFileObjectByChecksum(checksum: string, options?: ListFileObjectsOptions): Promise<FileObject | null>;
|
|
100
|
+
/**
|
|
101
|
+
* Mark a file deleted without touching the object.
|
|
102
|
+
*
|
|
103
|
+
* This is what the request path calls. Returns `false` when the row is already
|
|
104
|
+
* archived or absent, so a double-click is not an error.
|
|
105
|
+
*/
|
|
106
|
+
export declare function archiveFileObject(id: string, options?: FileQuerierOption): Promise<boolean>;
|
|
107
|
+
/** Deletes objects from wherever they live. `@venturekit/storage`'s
|
|
108
|
+
* `deleteMany` satisfies this. */
|
|
109
|
+
export type ObjectDeleter = (keys: string[]) => Promise<void>;
|
|
110
|
+
export interface SweepArchivedFilesOptions extends FileQuerierOption {
|
|
111
|
+
/** Removes the bytes. Called before the rows are deleted. */
|
|
112
|
+
deleteObjects: ObjectDeleter;
|
|
113
|
+
/**
|
|
114
|
+
* Grace period before an archived file is really removed. Default 604800 (7
|
|
115
|
+
* days) — long enough that "undo" is possible and that a bad bulk-archive can
|
|
116
|
+
* be caught before the bytes are gone.
|
|
117
|
+
*/
|
|
118
|
+
retainForSeconds?: number;
|
|
119
|
+
/** Rows per pass. Default 100. */
|
|
120
|
+
limit?: number;
|
|
121
|
+
}
|
|
122
|
+
export interface SweepResult {
|
|
123
|
+
/** Rows whose object and record were both removed. */
|
|
124
|
+
deleted: number;
|
|
125
|
+
/** Rows considered but left alone because the object deletion threw. */
|
|
126
|
+
remaining: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Delete the objects and rows of files archived longer ago than the grace
|
|
130
|
+
* period.
|
|
131
|
+
*
|
|
132
|
+
* **Objects first, then rows**, and the order is the whole design. There is no
|
|
133
|
+
* transaction spanning S3 and Postgres, so one of two failure modes has to be
|
|
134
|
+
* chosen:
|
|
135
|
+
*
|
|
136
|
+
* - rows first → an orphaned object nobody can account for, discoverable only
|
|
137
|
+
* by diffing the entire bucket against this table;
|
|
138
|
+
* - objects first → a row naming a key that 404s, discoverable by walking
|
|
139
|
+
* rows, and invisible to users because the row is already archived.
|
|
140
|
+
*
|
|
141
|
+
* The second is recoverable and the first is silent, so the sweep takes the
|
|
142
|
+
* second. It is re-runnable for the same reason: a crash after the delete
|
|
143
|
+
* re-deletes keys that are already gone, which S3 treats as success.
|
|
144
|
+
*
|
|
145
|
+
* If `deleteObjects` throws, the rows are left for the next pass rather than
|
|
146
|
+
* deleted anyway — which would produce exactly the orphan this ordering exists
|
|
147
|
+
* to avoid.
|
|
148
|
+
*/
|
|
149
|
+
export declare function sweepArchivedFiles(options: SweepArchivedFilesOptions): Promise<SweepResult>;
|
|
150
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/files/postgres.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — uploaded-object metadata.
|
|
3
|
+
*
|
|
4
|
+
* `@venturekit/storage` is complete on the object itself: put, get, head,
|
|
5
|
+
* presign, copy, image optimisation. What a bucket cannot tell you is anything a
|
|
6
|
+
* product needs to know about an upload — which tenant owns it, what it is for,
|
|
7
|
+
* who put it there, whether these exact bytes are already stored, and when it
|
|
8
|
+
* may be deleted. `ListObjectsV2` plus a key-naming convention is the usual
|
|
9
|
+
* substitute and it fails at the first "every document for this tenant, newest
|
|
10
|
+
* first".
|
|
11
|
+
*
|
|
12
|
+
* This module is the record; that package is the bytes. They are deliberately
|
|
13
|
+
* not coupled: `@venturekit/storage` has no `pg` dependency and this has no
|
|
14
|
+
* `@aws-sdk/client-s3` one, so {@link sweepArchivedFiles} takes the object
|
|
15
|
+
* deletion as a port and the consumer wires the two together:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* await sweepArchivedFiles({
|
|
19
|
+
* deleteObjects: (keys) => storage.deleteMany(keys).then(() => undefined),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { query } from '../query/index.js';
|
|
24
|
+
import { assertSqlIdentifier } from '../internal/identifier.js';
|
|
25
|
+
/** Default table name, created by `vk_data_004_file_object.sql`. */
|
|
26
|
+
export const DEFAULT_FILE_TABLE = 'vk_file_object';
|
|
27
|
+
const iso = (v) => v instanceof Date ? v.toISOString() : v;
|
|
28
|
+
function mapRow(r) {
|
|
29
|
+
return {
|
|
30
|
+
id: r.id,
|
|
31
|
+
tenantId: r.tenant_id,
|
|
32
|
+
purpose: r.purpose,
|
|
33
|
+
bucket: r.bucket,
|
|
34
|
+
objectKey: r.object_key,
|
|
35
|
+
contentType: r.content_type,
|
|
36
|
+
/* `bigint` arrives as a string from node-postgres. A file over 9 PB would
|
|
37
|
+
lose precision as a Number; a file over 9 PB is not the problem here. */
|
|
38
|
+
sizeBytes: typeof r.size_bytes === 'string' ? Number(r.size_bytes) : r.size_bytes,
|
|
39
|
+
checksum: r.checksum,
|
|
40
|
+
originalName: r.original_name,
|
|
41
|
+
uploadedBy: r.uploaded_by,
|
|
42
|
+
createdAt: iso(r.created_at),
|
|
43
|
+
archivedAt: r.archived_at === null ? null : iso(r.archived_at),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const COLUMNS = `id, tenant_id, purpose, bucket, object_key, content_type,
|
|
47
|
+
size_bytes, checksum, original_name, uploaded_by, created_at,
|
|
48
|
+
archived_at`;
|
|
49
|
+
function tableOf(options, context) {
|
|
50
|
+
const table = options.tableName ?? DEFAULT_FILE_TABLE;
|
|
51
|
+
assertSqlIdentifier(table, context);
|
|
52
|
+
return table;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Record an uploaded object.
|
|
56
|
+
*
|
|
57
|
+
* Call it in the transaction that also stores whatever references the file, so a
|
|
58
|
+
* rolled-back request cannot leave a row pointing at an object nothing uses —
|
|
59
|
+
* which is why `querier` is a required first argument here as in
|
|
60
|
+
* `appendToOutbox` and `enqueueJob`.
|
|
61
|
+
*
|
|
62
|
+
* **Record after the upload succeeds, not before.** The reverse ordering (row,
|
|
63
|
+
* then upload) produces a row whose object never arrived, and nothing
|
|
64
|
+
* distinguishes that from an object deleted later.
|
|
65
|
+
*/
|
|
66
|
+
export async function recordFileObject(querier, input, options = {}) {
|
|
67
|
+
const table = tableOf(options, 'recordFileObject.tableName');
|
|
68
|
+
const rows = await querier(`INSERT INTO ${table}
|
|
69
|
+
(tenant_id, purpose, bucket, object_key, content_type, size_bytes,
|
|
70
|
+
checksum, original_name, uploaded_by)
|
|
71
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
72
|
+
RETURNING id`, [
|
|
73
|
+
input.tenantId ?? null,
|
|
74
|
+
input.purpose,
|
|
75
|
+
input.bucket,
|
|
76
|
+
input.objectKey,
|
|
77
|
+
input.contentType,
|
|
78
|
+
input.sizeBytes,
|
|
79
|
+
input.checksum ?? null,
|
|
80
|
+
input.originalName ?? null,
|
|
81
|
+
input.uploadedBy ?? null,
|
|
82
|
+
]);
|
|
83
|
+
const id = rows[0]?.id;
|
|
84
|
+
if (!id) {
|
|
85
|
+
throw new Error('[venturekit/data] recordFileObject: insert returned no id');
|
|
86
|
+
}
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
/** One file by id. Archived rows are returned — callers may need to show them. */
|
|
90
|
+
export async function getFileObject(id, options = {}) {
|
|
91
|
+
const table = tableOf(options, 'getFileObject.tableName');
|
|
92
|
+
const run = options.querier ?? query;
|
|
93
|
+
const rows = await run(`SELECT ${COLUMNS} FROM ${table} WHERE id = $1`, [id]);
|
|
94
|
+
const row = rows[0];
|
|
95
|
+
return row ? mapRow(row) : null;
|
|
96
|
+
}
|
|
97
|
+
/** Live files for the current scope, newest first. */
|
|
98
|
+
export async function listFileObjects(options = {}) {
|
|
99
|
+
const table = tableOf(options, 'listFileObjects.tableName');
|
|
100
|
+
const run = options.querier ?? query;
|
|
101
|
+
const rows = await run(`SELECT ${COLUMNS}
|
|
102
|
+
FROM ${table}
|
|
103
|
+
WHERE ($1::text IS NULL OR purpose = $1)
|
|
104
|
+
AND ($3::boolean OR archived_at IS NULL)
|
|
105
|
+
AND (NOT $4::boolean OR tenant_id = ANY (vk_tenant_scope()))
|
|
106
|
+
ORDER BY created_at DESC
|
|
107
|
+
LIMIT $2`, [
|
|
108
|
+
options.purpose ?? null,
|
|
109
|
+
options.limit ?? 50,
|
|
110
|
+
options.includeArchived === true,
|
|
111
|
+
options.tenantScoped !== false,
|
|
112
|
+
]);
|
|
113
|
+
return rows.map(mapRow);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Find a live file with these exact bytes, for de-duplication on re-upload.
|
|
117
|
+
*
|
|
118
|
+
* Scoped to the tenant by default and deliberately so: matching a checksum
|
|
119
|
+
* across tenants would let one tenant's upload resolve to another's object,
|
|
120
|
+
* which is a cross-tenant read dressed up as an optimisation.
|
|
121
|
+
*/
|
|
122
|
+
export async function findFileObjectByChecksum(checksum, options = {}) {
|
|
123
|
+
const table = tableOf(options, 'findFileObjectByChecksum.tableName');
|
|
124
|
+
const run = options.querier ?? query;
|
|
125
|
+
const rows = await run(`SELECT ${COLUMNS}
|
|
126
|
+
FROM ${table}
|
|
127
|
+
WHERE checksum = $1
|
|
128
|
+
AND archived_at IS NULL
|
|
129
|
+
AND (NOT $2::boolean OR tenant_id = ANY (vk_tenant_scope()))
|
|
130
|
+
ORDER BY created_at
|
|
131
|
+
LIMIT 1`, [checksum, options.tenantScoped !== false]);
|
|
132
|
+
const row = rows[0];
|
|
133
|
+
return row ? mapRow(row) : null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Mark a file deleted without touching the object.
|
|
137
|
+
*
|
|
138
|
+
* This is what the request path calls. Returns `false` when the row is already
|
|
139
|
+
* archived or absent, so a double-click is not an error.
|
|
140
|
+
*/
|
|
141
|
+
export async function archiveFileObject(id, options = {}) {
|
|
142
|
+
const table = tableOf(options, 'archiveFileObject.tableName');
|
|
143
|
+
const run = options.querier ?? query;
|
|
144
|
+
const rows = await run(`UPDATE ${table}
|
|
145
|
+
SET archived_at = now(), updated_at = now()
|
|
146
|
+
WHERE id = $1 AND archived_at IS NULL
|
|
147
|
+
RETURNING id`, [id]);
|
|
148
|
+
return rows.length > 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Delete the objects and rows of files archived longer ago than the grace
|
|
152
|
+
* period.
|
|
153
|
+
*
|
|
154
|
+
* **Objects first, then rows**, and the order is the whole design. There is no
|
|
155
|
+
* transaction spanning S3 and Postgres, so one of two failure modes has to be
|
|
156
|
+
* chosen:
|
|
157
|
+
*
|
|
158
|
+
* - rows first → an orphaned object nobody can account for, discoverable only
|
|
159
|
+
* by diffing the entire bucket against this table;
|
|
160
|
+
* - objects first → a row naming a key that 404s, discoverable by walking
|
|
161
|
+
* rows, and invisible to users because the row is already archived.
|
|
162
|
+
*
|
|
163
|
+
* The second is recoverable and the first is silent, so the sweep takes the
|
|
164
|
+
* second. It is re-runnable for the same reason: a crash after the delete
|
|
165
|
+
* re-deletes keys that are already gone, which S3 treats as success.
|
|
166
|
+
*
|
|
167
|
+
* If `deleteObjects` throws, the rows are left for the next pass rather than
|
|
168
|
+
* deleted anyway — which would produce exactly the orphan this ordering exists
|
|
169
|
+
* to avoid.
|
|
170
|
+
*/
|
|
171
|
+
export async function sweepArchivedFiles(options) {
|
|
172
|
+
const table = tableOf(options, 'sweepArchivedFiles.tableName');
|
|
173
|
+
const run = options.querier ?? query;
|
|
174
|
+
const retain = options.retainForSeconds ?? 604_800;
|
|
175
|
+
const rows = await run(`SELECT id, object_key
|
|
176
|
+
FROM ${table}
|
|
177
|
+
WHERE archived_at IS NOT NULL
|
|
178
|
+
AND archived_at < now() - make_interval(secs => $1::double precision)
|
|
179
|
+
ORDER BY archived_at
|
|
180
|
+
LIMIT $2`, [retain, options.limit ?? 100]);
|
|
181
|
+
if (rows.length === 0)
|
|
182
|
+
return { deleted: 0, remaining: 0 };
|
|
183
|
+
try {
|
|
184
|
+
await options.deleteObjects(rows.map((r) => r.object_key));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* Leave every row of this batch. Deleting them now is what creates an
|
|
188
|
+
object nobody can account for. */
|
|
189
|
+
return { deleted: 0, remaining: rows.length };
|
|
190
|
+
}
|
|
191
|
+
await run(`DELETE FROM ${table} WHERE id = ANY($1::uuid[])`, [rows.map((r) => r.id)]);
|
|
192
|
+
return { deleted: rows.length, remaining: 0 };
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/files/postgres.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — Postgres idempotency store.
|
|
3
|
+
*
|
|
4
|
+
* Plugs into `@venturekit/runtime`'s `idempotencyMiddleware` for projects whose
|
|
5
|
+
* only datastore is Postgres. See `./postgres.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export { createPostgresIdempotencyStore, purgeExpiredIdempotencyRecords, DEFAULT_IDEMPOTENCY_TABLE, } from './postgres.js';
|
|
8
|
+
export type { IdempotencyRecord, PostgresIdempotencyStore, PostgresIdempotencyStoreOptions, } from './postgres.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/idempotency/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,8BAA8B,EAC9B,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,+BAA+B,GAChC,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — Postgres idempotency store.
|
|
3
|
+
*
|
|
4
|
+
* Plugs into `@venturekit/runtime`'s `idempotencyMiddleware` for projects whose
|
|
5
|
+
* only datastore is Postgres. See `./postgres.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export { createPostgresIdempotencyStore, purgeExpiredIdempotencyRecords, DEFAULT_IDEMPOTENCY_TABLE, } from './postgres.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/idempotency/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,8BAA8B,EAC9B,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @venturekit/data — Postgres idempotency store.
|
|
3
|
+
*
|
|
4
|
+
* `@venturekit/runtime`'s `idempotencyMiddleware` takes a pluggable
|
|
5
|
+
* `IdempotencyStore`; the package ships an in-memory one (per-process, so wrong
|
|
6
|
+
* for Lambda) and a DynamoDB one, and its own docblock has always advertised
|
|
7
|
+
* "DynamoDB, Postgres, in-memory, etc." without the middle one existing. A
|
|
8
|
+
* project whose only datastore is Postgres therefore had to add a DynamoDB
|
|
9
|
+
* table for four columns, or write this adapter itself.
|
|
10
|
+
*
|
|
11
|
+
* This is that adapter. It is deliberately typed structurally rather than
|
|
12
|
+
* against `@venturekit/runtime`'s interface: `runtime` depends on `data`, so
|
|
13
|
+
* importing the type here would invert the dependency. The shape is asserted by
|
|
14
|
+
* the tests instead, and `createPostgresIdempotencyStore()` is assignable to
|
|
15
|
+
* `IdempotencyStore` wherever the middleware wants one.
|
|
16
|
+
*
|
|
17
|
+
* ### Schema
|
|
18
|
+
*
|
|
19
|
+
* `vk_idempotency_record`, from this package's `vk_data_001_idempotency.sql`.
|
|
20
|
+
*
|
|
21
|
+
* ### One thing Postgres does not do for you
|
|
22
|
+
*
|
|
23
|
+
* DynamoDB expires rows itself via its TTL attribute. Postgres has no
|
|
24
|
+
* equivalent, so expired rows accumulate until something deletes them:
|
|
25
|
+
* {@link purgeExpiredIdempotencyRecords} is that something, and it wants a
|
|
26
|
+
* schedule. Reads are already correct without it — `get()` filters on
|
|
27
|
+
* `expires_at` — so a project that forgets pays in disk, not in behaviour.
|
|
28
|
+
*/
|
|
29
|
+
import type { Querier } from '../query/index.js';
|
|
30
|
+
/** Default table name. Overridable for projects that prefix or schema-qualify. */
|
|
31
|
+
export declare const DEFAULT_IDEMPOTENCY_TABLE = "vk_idempotency_record";
|
|
32
|
+
/**
|
|
33
|
+
* One stored request result. Mirrors `@venturekit/runtime`'s
|
|
34
|
+
* `IdempotencyRecord` field for field, including `expiresAt` as epoch
|
|
35
|
+
* milliseconds — the column is `timestamptz`, converted on both edges, because
|
|
36
|
+
* a bigint of millis is unreadable in `psql` and a timestamp is not.
|
|
37
|
+
*/
|
|
38
|
+
export interface IdempotencyRecord {
|
|
39
|
+
key: string;
|
|
40
|
+
response: string;
|
|
41
|
+
status: 'pending' | 'completed';
|
|
42
|
+
expiresAt: number;
|
|
43
|
+
}
|
|
44
|
+
export interface PostgresIdempotencyStoreOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Runs the statements. Defaults to this package's pooled `query`. Pass a
|
|
47
|
+
* transaction's `query` to make the record commit with the work it guards —
|
|
48
|
+
* which is the only way "the response was stored" and "the side effect
|
|
49
|
+
* happened" can be made to agree.
|
|
50
|
+
*/
|
|
51
|
+
querier?: Querier;
|
|
52
|
+
/** Table name. Validated as an identifier, since it is interpolated. */
|
|
53
|
+
tableName?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Namespace folded into every key.
|
|
56
|
+
*
|
|
57
|
+
* **Read this if the API is multi-tenant.** The middleware's default
|
|
58
|
+
* `headerKeyExtractor('idempotency-key')` takes the key from a client-supplied
|
|
59
|
+
* header, and the store is a flat key→response map. So two callers who happen
|
|
60
|
+
* to send the same key — `"1"`, `"retry"`, a uuid a client reuses — collide,
|
|
61
|
+
* and the second one is served the first one's cached response body. Across
|
|
62
|
+
* tenants that is a cross-tenant read.
|
|
63
|
+
*
|
|
64
|
+
* Supplying a scope makes the stored key `<scope>:<key>`, so collisions are
|
|
65
|
+
* confined to whatever the scope names. Pass the acting tenant id, or the
|
|
66
|
+
* principal, for anything a client can reach:
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* createPostgresIdempotencyStore({ scope: () => getCurrentTenantId() })
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* Left unset the behaviour matches the existing stores — unscoped — which is
|
|
73
|
+
* safe only when the key is server-generated or the API is single-tenant.
|
|
74
|
+
*/
|
|
75
|
+
scope?: () => string | null | undefined;
|
|
76
|
+
}
|
|
77
|
+
/** The store's four methods, as the middleware expects them. */
|
|
78
|
+
export interface PostgresIdempotencyStore {
|
|
79
|
+
get(key: string): Promise<IdempotencyRecord | null>;
|
|
80
|
+
save(record: IdempotencyRecord): Promise<void>;
|
|
81
|
+
update(key: string, updates: Partial<IdempotencyRecord>): Promise<void>;
|
|
82
|
+
delete(key: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a Postgres-backed idempotency store.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { idempotencyMiddleware } from '@venturekit/runtime';
|
|
90
|
+
* import { createPostgresIdempotencyStore } from '@venturekit/data/idempotency';
|
|
91
|
+
*
|
|
92
|
+
* const idempotency = idempotencyMiddleware({
|
|
93
|
+
* store: createPostgresIdempotencyStore({ scope: () => getCurrentTenantId() }),
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function createPostgresIdempotencyStore(options?: PostgresIdempotencyStoreOptions): PostgresIdempotencyStore;
|
|
98
|
+
/**
|
|
99
|
+
* Delete expired records. Returns how many went.
|
|
100
|
+
*
|
|
101
|
+
* Postgres has no TTL, so this is the counterpart to DynamoDB's automatic
|
|
102
|
+
* expiry and it needs a caller — a `schedules` intent running nightly is
|
|
103
|
+
* plenty. Nothing reads an expired row before it is deleted (`get()` filters
|
|
104
|
+
* on `expires_at`), so the only cost of never calling this is disk.
|
|
105
|
+
*/
|
|
106
|
+
export declare function purgeExpiredIdempotencyRecords(options?: Pick<PostgresIdempotencyStoreOptions, 'querier' | 'tableName'>): Promise<number>;
|
|
107
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/idempotency/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGjD,kFAAkF;AAClF,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,+BAA+B;IAC9C;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACzC;AAWD,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAYD;;;;;;;;;;;;GAYG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,GAAE,+BAAoC,GAC5C,wBAAwB,CAqF1B;AAED;;;;;;;GAOG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,GAAE,IAAI,CAAC,+BAA+B,EAAE,SAAS,GAAG,WAAW,CAAM,GAC3E,OAAO,CAAC,MAAM,CAAC,CAQjB"}
|