@copypatch/storage-postgres 2.0.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 +21 -0
- package/README.md +59 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +536 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Efe Arabacı
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# `@copypatch/storage-postgres`
|
|
2
|
+
|
|
3
|
+
PostgreSQL persistence for CopyPatch v2. The adapter uses the vendor-neutral `pg` driver and implements the `CopyPatchPersistence` contract from `@copypatch/core`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @copypatch/storage-postgres pg
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node.js 20 or newer is required.
|
|
12
|
+
|
|
13
|
+
## Connection string
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createPostgresPersistence } from '@copypatch/storage-postgres';
|
|
17
|
+
|
|
18
|
+
const persistence = createPostgresPersistence({
|
|
19
|
+
connectionString: process.env.DATABASE_URL!,
|
|
20
|
+
schema: 'copypatch',
|
|
21
|
+
publishingMode: 'draft',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await persistence.migrate();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The connection pool is created lazily. Call `close()` during application shutdown when the adapter owns the pool.
|
|
28
|
+
|
|
29
|
+
## Existing pool
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Pool } from 'pg';
|
|
33
|
+
import { createPostgresPersistence } from '@copypatch/storage-postgres';
|
|
34
|
+
|
|
35
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
36
|
+
const persistence = createPostgresPersistence({ pool, schema: 'copypatch' });
|
|
37
|
+
|
|
38
|
+
await persistence.migrate();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An injected pool remains owned by the caller and is not closed by `persistence.close()`.
|
|
42
|
+
|
|
43
|
+
## Operational behavior
|
|
44
|
+
|
|
45
|
+
- Run `migrate()` before serving traffic. Migration is idempotent and serialized with a PostgreSQL advisory transaction lock.
|
|
46
|
+
- Content compare-and-swap mutations lock per locale and check both published and draft revisions.
|
|
47
|
+
- Public reads do not initialize rows or otherwise write to PostgreSQL.
|
|
48
|
+
- Session APIs accept and persist token hashes only. Hash raw session, CSRF, address, or other rate-limit identifiers before calling the adapter.
|
|
49
|
+
- Rate-limit counters are persistent and updated atomically across application instances.
|
|
50
|
+
|
|
51
|
+
## PostgreSQL contract tests
|
|
52
|
+
|
|
53
|
+
The live database suite runs only when `COPYPATCH_TEST_POSTGRES_URL` is set:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
COPYPATCH_TEST_POSTGRES_URL=postgres://postgres:postgres@localhost:5432/postgres pnpm test
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each run creates a randomized schema and drops only that schema during cleanup.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ContentSnapshot, type CopyPatchPersistence, type DiscardDraftsCommand, type DiscardDraftsResult, type EditorSnapshot, type PersistenceHealth, type PersistenceMutationResult, type PublishDraftsCommand, type PublishDraftsResult, type PublishingMode, type RateLimitDecision, type RateLimitInput, type SaveDraftsCommand, type SaveDraftsResult, type SessionTouch, type StoredSession } from '@copypatch/core';
|
|
2
|
+
export type { StoredSession } from '@copypatch/core';
|
|
3
|
+
export interface PgQueryResult<TRow extends Record<string, unknown> = Record<string, unknown>> {
|
|
4
|
+
rows: TRow[];
|
|
5
|
+
rowCount: number | null;
|
|
6
|
+
}
|
|
7
|
+
export interface PgQueryable {
|
|
8
|
+
query<TRow extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgQueryResult<TRow>>;
|
|
9
|
+
}
|
|
10
|
+
export interface PgClientLike extends PgQueryable {
|
|
11
|
+
release(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface PgPoolLike extends PgQueryable {
|
|
14
|
+
connect(): Promise<PgClientLike>;
|
|
15
|
+
end?(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface PostgresPersistenceOptions {
|
|
18
|
+
pool?: PgPoolLike;
|
|
19
|
+
connectionString?: string;
|
|
20
|
+
schema?: string;
|
|
21
|
+
publishingMode?: PublishingMode;
|
|
22
|
+
maxTextLength?: number;
|
|
23
|
+
}
|
|
24
|
+
type PoolFactory = () => Promise<PgPoolLike>;
|
|
25
|
+
export declare class PostgresPersistence implements CopyPatchPersistence {
|
|
26
|
+
readonly schema: string;
|
|
27
|
+
readonly publishingMode: PublishingMode;
|
|
28
|
+
private readonly schemaSql;
|
|
29
|
+
private readonly maxTextLength;
|
|
30
|
+
private readonly poolSource;
|
|
31
|
+
private readonly ownsPool;
|
|
32
|
+
private poolPromise;
|
|
33
|
+
constructor(pool: PgPoolLike | PoolFactory, options?: Omit<PostgresPersistenceOptions, 'pool' | 'connectionString'>, ownsPool?: boolean);
|
|
34
|
+
migrate(): Promise<void>;
|
|
35
|
+
health(): Promise<PersistenceHealth>;
|
|
36
|
+
readPublished(locale: string): Promise<ContentSnapshot>;
|
|
37
|
+
readEditor(locale: string): Promise<EditorSnapshot>;
|
|
38
|
+
saveDrafts(command: SaveDraftsCommand): Promise<PersistenceMutationResult<SaveDraftsResult>>;
|
|
39
|
+
publishDrafts(command: PublishDraftsCommand): Promise<PersistenceMutationResult<PublishDraftsResult>>;
|
|
40
|
+
discardDrafts(command: DiscardDraftsCommand): Promise<PersistenceMutationResult<DiscardDraftsResult>>;
|
|
41
|
+
createSession(session: StoredSession): Promise<void>;
|
|
42
|
+
readSession(tokenHash: string): Promise<StoredSession | null>;
|
|
43
|
+
touchSession(tokenHash: string, update: SessionTouch): Promise<StoredSession | null>;
|
|
44
|
+
deleteSession(tokenHash: string): Promise<void>;
|
|
45
|
+
consumeRateLimit(input: RateLimitInput): Promise<RateLimitDecision>;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
private getPool;
|
|
48
|
+
private table;
|
|
49
|
+
private readSnapshot;
|
|
50
|
+
private lockState;
|
|
51
|
+
private ensureState;
|
|
52
|
+
}
|
|
53
|
+
export declare function createPostgresPersistence(input: string | PgPoolLike | PostgresPersistenceOptions): PostgresPersistence;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { isValidContentKey, isValidLocale, normalizeText, } from '@copypatch/core';
|
|
2
|
+
const DEFAULT_SCHEMA = 'copypatch';
|
|
3
|
+
const DEFAULT_MAX_TEXT_LENGTH = 10_000;
|
|
4
|
+
const SCHEMA_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
5
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
6
|
+
const REQUIRED_TABLES = ['content_entries', 'content_state', 'rate_limits', 'sessions'];
|
|
7
|
+
export class PostgresPersistence {
|
|
8
|
+
schema;
|
|
9
|
+
publishingMode;
|
|
10
|
+
schemaSql;
|
|
11
|
+
maxTextLength;
|
|
12
|
+
poolSource;
|
|
13
|
+
ownsPool;
|
|
14
|
+
poolPromise;
|
|
15
|
+
constructor(pool, options = {}, ownsPool = false) {
|
|
16
|
+
this.schema = validateSchemaName(options.schema ?? DEFAULT_SCHEMA);
|
|
17
|
+
this.schemaSql = quoteIdentifier(this.schema);
|
|
18
|
+
this.publishingMode = options.publishingMode ?? 'draft';
|
|
19
|
+
this.maxTextLength = options.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH;
|
|
20
|
+
if (!Number.isSafeInteger(this.maxTextLength) || this.maxTextLength < 1) {
|
|
21
|
+
throw new TypeError('maxTextLength must be a positive safe integer');
|
|
22
|
+
}
|
|
23
|
+
this.poolSource = pool;
|
|
24
|
+
this.ownsPool = ownsPool;
|
|
25
|
+
}
|
|
26
|
+
async migrate() {
|
|
27
|
+
const pool = await this.getPool();
|
|
28
|
+
const client = await pool.connect();
|
|
29
|
+
try {
|
|
30
|
+
await client.query('BEGIN');
|
|
31
|
+
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
|
|
32
|
+
`copypatch:migrate:${this.schema}`,
|
|
33
|
+
]);
|
|
34
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${this.schemaSql}`);
|
|
35
|
+
await client.query(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS ${this.table('content_state')} (
|
|
37
|
+
locale varchar(35) PRIMARY KEY,
|
|
38
|
+
published_revision integer NOT NULL DEFAULT 1 CHECK (published_revision >= 1),
|
|
39
|
+
draft_revision integer NOT NULL DEFAULT 1 CHECK (draft_revision >= 1),
|
|
40
|
+
updated_at bigint NOT NULL
|
|
41
|
+
)
|
|
42
|
+
`);
|
|
43
|
+
await client.query(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS ${this.table('content_entries')} (
|
|
45
|
+
locale varchar(35) NOT NULL REFERENCES ${this.table('content_state')}(locale) ON DELETE CASCADE,
|
|
46
|
+
content_key varchar(160) NOT NULL,
|
|
47
|
+
published_text text,
|
|
48
|
+
draft_text text,
|
|
49
|
+
created_at bigint NOT NULL,
|
|
50
|
+
updated_at bigint NOT NULL,
|
|
51
|
+
PRIMARY KEY (locale, content_key)
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
await client.query(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS ${this.table('sessions')} (
|
|
56
|
+
token_hash text PRIMARY KEY,
|
|
57
|
+
csrf_token_hash text NOT NULL,
|
|
58
|
+
subject text NOT NULL,
|
|
59
|
+
roles text[] NOT NULL,
|
|
60
|
+
created_at bigint NOT NULL,
|
|
61
|
+
last_seen_at bigint NOT NULL,
|
|
62
|
+
idle_expires_at bigint NOT NULL,
|
|
63
|
+
absolute_expires_at bigint NOT NULL
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
await client.query(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS ${this.table('rate_limits')} (
|
|
68
|
+
key_hash text PRIMARY KEY,
|
|
69
|
+
attempts integer NOT NULL CHECK (attempts >= 1),
|
|
70
|
+
reset_at bigint NOT NULL
|
|
71
|
+
)
|
|
72
|
+
`);
|
|
73
|
+
await client.query('COMMIT');
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
await rollback(client);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
client.release();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async health() {
|
|
84
|
+
try {
|
|
85
|
+
const pool = await this.getPool();
|
|
86
|
+
const result = await pool.query(`SELECT table_name
|
|
87
|
+
FROM information_schema.tables
|
|
88
|
+
WHERE table_schema = $1 AND table_name = ANY($2::text[])`, [this.schema, [...REQUIRED_TABLES]]);
|
|
89
|
+
const found = new Set(result.rows.map((row) => row.table_name));
|
|
90
|
+
const missing = REQUIRED_TABLES.filter((table) => !found.has(table));
|
|
91
|
+
if (missing.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
message: `PostgreSQL schema "${this.schema}" is not migrated; missing tables: ${missing.join(', ')}.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return { ok: false, message: errorMessage(error) };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async readPublished(locale) {
|
|
104
|
+
assertLocale(locale);
|
|
105
|
+
return this.readSnapshot(async (client) => {
|
|
106
|
+
const stateResult = await client.query(`SELECT published_revision, draft_revision FROM ${this.table('content_state')} WHERE locale = $1`, [locale]);
|
|
107
|
+
const entriesResult = await client.query(`SELECT content_key, published_text, draft_text
|
|
108
|
+
FROM ${this.table('content_entries')}
|
|
109
|
+
WHERE locale = $1 AND published_text IS NOT NULL
|
|
110
|
+
ORDER BY content_key`, [locale]);
|
|
111
|
+
const content = {};
|
|
112
|
+
for (const row of entriesResult.rows) {
|
|
113
|
+
if (row.published_text !== null)
|
|
114
|
+
content[row.content_key] = row.published_text;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
revision: toNumber(stateResult.rows[0]?.published_revision ?? 1),
|
|
118
|
+
content,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async readEditor(locale) {
|
|
123
|
+
assertLocale(locale);
|
|
124
|
+
return this.readSnapshot(async (client) => {
|
|
125
|
+
const stateResult = await client.query(`SELECT published_revision, draft_revision FROM ${this.table('content_state')} WHERE locale = $1`, [locale]);
|
|
126
|
+
const entriesResult = await client.query(`SELECT content_key, published_text, draft_text
|
|
127
|
+
FROM ${this.table('content_entries')}
|
|
128
|
+
WHERE locale = $1
|
|
129
|
+
ORDER BY content_key`, [locale]);
|
|
130
|
+
const state = stateResult.rows[0];
|
|
131
|
+
const published = {};
|
|
132
|
+
const drafts = {};
|
|
133
|
+
for (const row of entriesResult.rows) {
|
|
134
|
+
if (row.published_text !== null)
|
|
135
|
+
published[row.content_key] = row.published_text;
|
|
136
|
+
if (row.draft_text !== null)
|
|
137
|
+
drafts[row.content_key] = row.draft_text;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
locale,
|
|
141
|
+
publishedRevision: toNumber(state?.published_revision ?? 1),
|
|
142
|
+
draftRevision: toNumber(state?.draft_revision ?? 1),
|
|
143
|
+
publishingMode: this.publishingMode,
|
|
144
|
+
published,
|
|
145
|
+
drafts,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async saveDrafts(command) {
|
|
150
|
+
assertCommandRevisions(command);
|
|
151
|
+
validateChanges(command, this.maxTextLength);
|
|
152
|
+
const pool = await this.getPool();
|
|
153
|
+
const client = await pool.connect();
|
|
154
|
+
try {
|
|
155
|
+
await client.query('BEGIN');
|
|
156
|
+
const state = await this.lockState(client, command.locale);
|
|
157
|
+
if (!revisionsMatch(state, command)) {
|
|
158
|
+
await client.query('ROLLBACK');
|
|
159
|
+
return { status: 'conflict', latest: await this.readEditor(command.locale) };
|
|
160
|
+
}
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
await this.ensureState(client, command.locale, state, now);
|
|
163
|
+
for (const change of command.changes) {
|
|
164
|
+
await client.query(`INSERT INTO ${this.table('content_entries')}
|
|
165
|
+
(locale, content_key, published_text, draft_text, created_at, updated_at)
|
|
166
|
+
VALUES ($1, $2, NULL, $3, $4, $4)
|
|
167
|
+
ON CONFLICT (locale, content_key) DO UPDATE
|
|
168
|
+
SET draft_text = EXCLUDED.draft_text, updated_at = EXCLUDED.updated_at`, [command.locale, change.key, normalizeText(change.text, true), now]);
|
|
169
|
+
}
|
|
170
|
+
const nextDraftRevision = state.draftRevision + 1;
|
|
171
|
+
await client.query(`UPDATE ${this.table('content_state')}
|
|
172
|
+
SET draft_revision = $2, updated_at = $3
|
|
173
|
+
WHERE locale = $1`, [command.locale, nextDraftRevision, now]);
|
|
174
|
+
await client.query('COMMIT');
|
|
175
|
+
return {
|
|
176
|
+
status: 'ok',
|
|
177
|
+
value: {
|
|
178
|
+
publishedRevision: state.publishedRevision,
|
|
179
|
+
draftRevision: nextDraftRevision,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
await rollback(client);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
client.release();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async publishDrafts(command) {
|
|
192
|
+
assertCommandRevisions(command);
|
|
193
|
+
const pool = await this.getPool();
|
|
194
|
+
const client = await pool.connect();
|
|
195
|
+
try {
|
|
196
|
+
await client.query('BEGIN');
|
|
197
|
+
const state = await this.lockState(client, command.locale);
|
|
198
|
+
if (!revisionsMatch(state, command)) {
|
|
199
|
+
await client.query('ROLLBACK');
|
|
200
|
+
return { status: 'conflict', latest: await this.readEditor(command.locale) };
|
|
201
|
+
}
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
await this.ensureState(client, command.locale, state, now);
|
|
204
|
+
const promoted = await client.query(`UPDATE ${this.table('content_entries')}
|
|
205
|
+
SET published_text = draft_text, draft_text = NULL, updated_at = $2
|
|
206
|
+
WHERE locale = $1 AND draft_text IS NOT NULL`, [command.locale, now]);
|
|
207
|
+
const nextPublishedRevision = state.publishedRevision + 1;
|
|
208
|
+
const nextDraftRevision = state.draftRevision + 1;
|
|
209
|
+
await client.query(`UPDATE ${this.table('content_state')}
|
|
210
|
+
SET published_revision = $2, draft_revision = $3, updated_at = $4
|
|
211
|
+
WHERE locale = $1`, [command.locale, nextPublishedRevision, nextDraftRevision, now]);
|
|
212
|
+
await client.query('COMMIT');
|
|
213
|
+
return {
|
|
214
|
+
status: 'ok',
|
|
215
|
+
value: {
|
|
216
|
+
publishedRevision: nextPublishedRevision,
|
|
217
|
+
draftRevision: nextDraftRevision,
|
|
218
|
+
promotedCount: promoted.rowCount ?? 0,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
await rollback(client);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
client.release();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async discardDrafts(command) {
|
|
231
|
+
assertCommandRevisions(command);
|
|
232
|
+
const pool = await this.getPool();
|
|
233
|
+
const client = await pool.connect();
|
|
234
|
+
try {
|
|
235
|
+
await client.query('BEGIN');
|
|
236
|
+
const state = await this.lockState(client, command.locale);
|
|
237
|
+
if (!revisionsMatch(state, command)) {
|
|
238
|
+
await client.query('ROLLBACK');
|
|
239
|
+
return { status: 'conflict', latest: await this.readEditor(command.locale) };
|
|
240
|
+
}
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
await this.ensureState(client, command.locale, state, now);
|
|
243
|
+
const discarded = await client.query(`UPDATE ${this.table('content_entries')}
|
|
244
|
+
SET draft_text = NULL, updated_at = $2
|
|
245
|
+
WHERE locale = $1 AND draft_text IS NOT NULL`, [command.locale, now]);
|
|
246
|
+
const nextDraftRevision = state.draftRevision + 1;
|
|
247
|
+
await client.query(`UPDATE ${this.table('content_state')}
|
|
248
|
+
SET draft_revision = $2, updated_at = $3
|
|
249
|
+
WHERE locale = $1`, [command.locale, nextDraftRevision, now]);
|
|
250
|
+
await client.query('COMMIT');
|
|
251
|
+
return {
|
|
252
|
+
status: 'ok',
|
|
253
|
+
value: {
|
|
254
|
+
publishedRevision: state.publishedRevision,
|
|
255
|
+
draftRevision: nextDraftRevision,
|
|
256
|
+
discardedCount: discarded.rowCount ?? 0,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
await rollback(client);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
client.release();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async createSession(session) {
|
|
269
|
+
validateSession(session);
|
|
270
|
+
const pool = await this.getPool();
|
|
271
|
+
await pool.query(`INSERT INTO ${this.table('sessions')}
|
|
272
|
+
(token_hash, csrf_token_hash, subject, roles, created_at, last_seen_at, idle_expires_at, absolute_expires_at)
|
|
273
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [
|
|
274
|
+
session.tokenHash,
|
|
275
|
+
session.csrfTokenHash,
|
|
276
|
+
session.subject,
|
|
277
|
+
[...session.roles],
|
|
278
|
+
session.createdAt,
|
|
279
|
+
session.lastSeenAt,
|
|
280
|
+
session.idleExpiresAt,
|
|
281
|
+
session.absoluteExpiresAt,
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
async readSession(tokenHash) {
|
|
285
|
+
assertHash('tokenHash', tokenHash);
|
|
286
|
+
const pool = await this.getPool();
|
|
287
|
+
const queryResult = await pool.query(`SELECT token_hash, csrf_token_hash, subject, roles,
|
|
288
|
+
created_at, last_seen_at, idle_expires_at, absolute_expires_at
|
|
289
|
+
FROM ${this.table('sessions')}
|
|
290
|
+
WHERE token_hash = $1`, [tokenHash]);
|
|
291
|
+
const row = queryResult.rows[0];
|
|
292
|
+
return row ? mapSession(row) : null;
|
|
293
|
+
}
|
|
294
|
+
async touchSession(tokenHash, update) {
|
|
295
|
+
assertHash('tokenHash', tokenHash);
|
|
296
|
+
assertTimestamp('lastSeenAt', update.lastSeenAt);
|
|
297
|
+
assertTimestamp('idleExpiresAt', update.idleExpiresAt);
|
|
298
|
+
if (update.csrfTokenHash !== undefined)
|
|
299
|
+
assertHash('csrfTokenHash', update.csrfTokenHash);
|
|
300
|
+
const pool = await this.getPool();
|
|
301
|
+
const values = [tokenHash, update.lastSeenAt, update.idleExpiresAt];
|
|
302
|
+
const csrfAssignment = update.csrfTokenHash === undefined ? '' : ', csrf_token_hash = $4';
|
|
303
|
+
if (update.csrfTokenHash !== undefined)
|
|
304
|
+
values.push(update.csrfTokenHash);
|
|
305
|
+
const queryResult = await pool.query(`UPDATE ${this.table('sessions')}
|
|
306
|
+
SET last_seen_at = $2, idle_expires_at = $3${csrfAssignment}
|
|
307
|
+
WHERE token_hash = $1
|
|
308
|
+
RETURNING token_hash, csrf_token_hash, subject, roles,
|
|
309
|
+
created_at, last_seen_at, idle_expires_at, absolute_expires_at`, values);
|
|
310
|
+
const row = queryResult.rows[0];
|
|
311
|
+
return row ? mapSession(row) : null;
|
|
312
|
+
}
|
|
313
|
+
async deleteSession(tokenHash) {
|
|
314
|
+
assertHash('tokenHash', tokenHash);
|
|
315
|
+
const pool = await this.getPool();
|
|
316
|
+
await pool.query(`DELETE FROM ${this.table('sessions')} WHERE token_hash = $1`, [tokenHash]);
|
|
317
|
+
}
|
|
318
|
+
async consumeRateLimit(input) {
|
|
319
|
+
validateRateLimit(input);
|
|
320
|
+
const pool = await this.getPool();
|
|
321
|
+
const client = await pool.connect();
|
|
322
|
+
try {
|
|
323
|
+
await client.query('BEGIN');
|
|
324
|
+
const resetAt = input.now + input.windowMs;
|
|
325
|
+
const queryResult = await client.query(`INSERT INTO ${this.table('rate_limits')} AS current_limit (key_hash, attempts, reset_at)
|
|
326
|
+
VALUES ($1, 1, $2)
|
|
327
|
+
ON CONFLICT (key_hash) DO UPDATE
|
|
328
|
+
SET attempts = CASE
|
|
329
|
+
WHEN current_limit.reset_at <= $3 THEN 1
|
|
330
|
+
ELSE current_limit.attempts + 1
|
|
331
|
+
END,
|
|
332
|
+
reset_at = CASE
|
|
333
|
+
WHEN current_limit.reset_at <= $3 THEN $2
|
|
334
|
+
ELSE current_limit.reset_at
|
|
335
|
+
END
|
|
336
|
+
RETURNING attempts, reset_at`, [input.keyHash, resetAt, input.now]);
|
|
337
|
+
const row = queryResult.rows[0];
|
|
338
|
+
if (!row)
|
|
339
|
+
throw new Error('PostgreSQL rate-limit upsert returned no row');
|
|
340
|
+
await client.query('COMMIT');
|
|
341
|
+
const attempts = toNumber(row.attempts);
|
|
342
|
+
return {
|
|
343
|
+
allowed: attempts <= input.limit,
|
|
344
|
+
remaining: Math.max(0, input.limit - attempts),
|
|
345
|
+
resetAt: toNumber(row.reset_at),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
await rollback(client);
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
client.release();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async close() {
|
|
357
|
+
if (!this.ownsPool || !this.poolPromise)
|
|
358
|
+
return;
|
|
359
|
+
const pool = await this.poolPromise;
|
|
360
|
+
await pool.end?.();
|
|
361
|
+
}
|
|
362
|
+
async getPool() {
|
|
363
|
+
if (!this.poolPromise) {
|
|
364
|
+
this.poolPromise =
|
|
365
|
+
typeof this.poolSource === 'function' ? this.poolSource() : Promise.resolve(this.poolSource);
|
|
366
|
+
}
|
|
367
|
+
return this.poolPromise;
|
|
368
|
+
}
|
|
369
|
+
table(name) {
|
|
370
|
+
return `${this.schemaSql}.${quoteIdentifier(name)}`;
|
|
371
|
+
}
|
|
372
|
+
async readSnapshot(read) {
|
|
373
|
+
const pool = await this.getPool();
|
|
374
|
+
const client = await pool.connect();
|
|
375
|
+
try {
|
|
376
|
+
await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
377
|
+
const snapshot = await read(client);
|
|
378
|
+
await client.query('COMMIT');
|
|
379
|
+
return snapshot;
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
await rollback(client);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
client.release();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async lockState(client, locale) {
|
|
390
|
+
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
|
|
391
|
+
`copypatch:content:${this.schema}:${locale}`,
|
|
392
|
+
]);
|
|
393
|
+
const queryResult = await client.query(`SELECT published_revision, draft_revision
|
|
394
|
+
FROM ${this.table('content_state')}
|
|
395
|
+
WHERE locale = $1
|
|
396
|
+
FOR UPDATE`, [locale]);
|
|
397
|
+
const row = queryResult.rows[0];
|
|
398
|
+
return {
|
|
399
|
+
publishedRevision: toNumber(row?.published_revision ?? 1),
|
|
400
|
+
draftRevision: toNumber(row?.draft_revision ?? 1),
|
|
401
|
+
exists: row !== undefined,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async ensureState(client, locale, state, now) {
|
|
405
|
+
if (state.exists)
|
|
406
|
+
return;
|
|
407
|
+
await client.query(`INSERT INTO ${this.table('content_state')}
|
|
408
|
+
(locale, published_revision, draft_revision, updated_at)
|
|
409
|
+
VALUES ($1, 1, 1, $2)`, [locale, now]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
export function createPostgresPersistence(input) {
|
|
413
|
+
if (typeof input === 'string') {
|
|
414
|
+
return fromConnectionString(input, {});
|
|
415
|
+
}
|
|
416
|
+
if (isPool(input)) {
|
|
417
|
+
return new PostgresPersistence(input);
|
|
418
|
+
}
|
|
419
|
+
const { pool, connectionString, ...options } = input;
|
|
420
|
+
if ((pool === undefined) === (connectionString === undefined)) {
|
|
421
|
+
throw new TypeError('Provide exactly one of pool or connectionString');
|
|
422
|
+
}
|
|
423
|
+
return pool ? new PostgresPersistence(pool, options) : fromConnectionString(connectionString, options);
|
|
424
|
+
}
|
|
425
|
+
function fromConnectionString(connectionString, options) {
|
|
426
|
+
if (connectionString.trim() === '')
|
|
427
|
+
throw new TypeError('connectionString must not be empty');
|
|
428
|
+
const factory = async () => {
|
|
429
|
+
const moduleName = 'pg';
|
|
430
|
+
const pgModule = (await import(moduleName));
|
|
431
|
+
return new pgModule.Pool({ connectionString });
|
|
432
|
+
};
|
|
433
|
+
return new PostgresPersistence(factory, options, true);
|
|
434
|
+
}
|
|
435
|
+
function isPool(value) {
|
|
436
|
+
return typeof value.query === 'function';
|
|
437
|
+
}
|
|
438
|
+
function validateSchemaName(schema) {
|
|
439
|
+
if (!SCHEMA_NAME.test(schema))
|
|
440
|
+
throw new TypeError(`Invalid PostgreSQL schema name: ${schema}`);
|
|
441
|
+
return schema;
|
|
442
|
+
}
|
|
443
|
+
function quoteIdentifier(identifier) {
|
|
444
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
445
|
+
}
|
|
446
|
+
function assertLocale(locale) {
|
|
447
|
+
if (!isValidLocale(locale))
|
|
448
|
+
throw new TypeError(`Invalid locale: ${locale}`);
|
|
449
|
+
}
|
|
450
|
+
function assertCommandRevisions(command) {
|
|
451
|
+
assertLocale(command.locale);
|
|
452
|
+
assertRevision('expectedPublishedRevision', command.expectedPublishedRevision);
|
|
453
|
+
assertRevision('expectedDraftRevision', command.expectedDraftRevision);
|
|
454
|
+
}
|
|
455
|
+
function assertRevision(name, value) {
|
|
456
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
457
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function validateChanges(command, maxTextLength) {
|
|
461
|
+
for (const change of command.changes) {
|
|
462
|
+
if (!isValidContentKey(change.key))
|
|
463
|
+
throw new TypeError(`Invalid content key: ${change.key}`);
|
|
464
|
+
if (typeof change.text !== 'string' || change.text.length > maxTextLength) {
|
|
465
|
+
throw new TypeError(`Text exceeds maximum allowed length of ${maxTextLength}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function revisionsMatch(state, command) {
|
|
470
|
+
return (state.publishedRevision === command.expectedPublishedRevision &&
|
|
471
|
+
state.draftRevision === command.expectedDraftRevision);
|
|
472
|
+
}
|
|
473
|
+
function validateSession(session) {
|
|
474
|
+
assertHash('tokenHash', session.tokenHash);
|
|
475
|
+
assertHash('csrfTokenHash', session.csrfTokenHash);
|
|
476
|
+
if (session.subject.length === 0)
|
|
477
|
+
throw new TypeError('subject must not be empty');
|
|
478
|
+
if (session.roles.length === 0)
|
|
479
|
+
throw new TypeError('roles must not be empty');
|
|
480
|
+
assertTimestamp('createdAt', session.createdAt);
|
|
481
|
+
assertTimestamp('lastSeenAt', session.lastSeenAt);
|
|
482
|
+
assertTimestamp('idleExpiresAt', session.idleExpiresAt);
|
|
483
|
+
assertTimestamp('absoluteExpiresAt', session.absoluteExpiresAt);
|
|
484
|
+
}
|
|
485
|
+
function assertHash(name, value) {
|
|
486
|
+
if (!SHA256_HEX.test(value))
|
|
487
|
+
throw new TypeError(`${name} must be a lowercase SHA-256 hex digest.`);
|
|
488
|
+
}
|
|
489
|
+
function assertTimestamp(name, value) {
|
|
490
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
491
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function validateRateLimit(input) {
|
|
495
|
+
assertHash('keyHash', input.keyHash);
|
|
496
|
+
if (!Number.isSafeInteger(input.limit) || input.limit < 1) {
|
|
497
|
+
throw new TypeError('limit must be a positive safe integer');
|
|
498
|
+
}
|
|
499
|
+
if (!Number.isSafeInteger(input.windowMs) || input.windowMs < 1) {
|
|
500
|
+
throw new TypeError('windowMs must be a positive safe integer');
|
|
501
|
+
}
|
|
502
|
+
assertTimestamp('now', input.now);
|
|
503
|
+
if (!Number.isSafeInteger(input.now + input.windowMs)) {
|
|
504
|
+
throw new TypeError('rate-limit resetAt exceeds the safe integer range');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function mapSession(row) {
|
|
508
|
+
return {
|
|
509
|
+
tokenHash: row.token_hash,
|
|
510
|
+
csrfTokenHash: row.csrf_token_hash,
|
|
511
|
+
subject: row.subject,
|
|
512
|
+
roles: row.roles,
|
|
513
|
+
createdAt: toNumber(row.created_at),
|
|
514
|
+
lastSeenAt: toNumber(row.last_seen_at),
|
|
515
|
+
idleExpiresAt: toNumber(row.idle_expires_at),
|
|
516
|
+
absoluteExpiresAt: toNumber(row.absolute_expires_at),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function toNumber(value) {
|
|
520
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
521
|
+
if (!Number.isSafeInteger(parsed))
|
|
522
|
+
throw new Error(`PostgreSQL returned an unsafe integer: ${value}`);
|
|
523
|
+
return parsed;
|
|
524
|
+
}
|
|
525
|
+
async function rollback(client) {
|
|
526
|
+
try {
|
|
527
|
+
await client.query('ROLLBACK');
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
// Preserve the operation error; PostgreSQL will discard the broken connection if needed.
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function errorMessage(error) {
|
|
534
|
+
return error instanceof Error ? error.message : String(error);
|
|
535
|
+
}
|
|
536
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@copypatch/storage-postgres",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "PostgreSQL persistence adapter for CopyPatch",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"homepage": "https://copypatch.vercel.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Woffluon/CopyPatch.git",
|
|
11
|
+
"directory": "packages/storage-postgres"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Woffluon/CopyPatch/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"copypatch",
|
|
18
|
+
"postgresql",
|
|
19
|
+
"persistence",
|
|
20
|
+
"content-editing"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -b tsconfig.json",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run tests",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@copypatch/core": "2.0.0",
|
|
50
|
+
"pg": "^8.16.3"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"@types/pg": "^8.15.5",
|
|
55
|
+
"typescript": "^5.8.0",
|
|
56
|
+
"vitest": "^3.2.7"
|
|
57
|
+
}
|
|
58
|
+
}
|