@affordance/pg 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/admin.d.ts +5 -0
- package/dist/admin.js +12 -0
- package/dist/admin.js.map +1 -0
- package/dist/bootstrap.d.ts +57 -0
- package/dist/bootstrap.js +268 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/correlation.d.ts +16 -0
- package/dist/correlation.js +62 -0
- package/dist/correlation.js.map +1 -0
- package/dist/delivery.d.ts +30 -0
- package/dist/delivery.js +97 -0
- package/dist/delivery.js.map +1 -0
- package/dist/execution.d.ts +4 -0
- package/dist/execution.js +89 -0
- package/dist/execution.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/journal.d.ts +9 -0
- package/dist/journal.js +94 -0
- package/dist/journal.js.map +1 -0
- package/dist/listing.d.ts +7 -0
- package/dist/listing.js +49 -0
- package/dist/listing.js.map +1 -0
- package/dist/migration.d.ts +7 -0
- package/dist/migration.js +38 -0
- package/dist/migration.js.map +1 -0
- package/dist/queryable.d.ts +61 -0
- package/dist/queryable.js +31 -0
- package/dist/queryable.js.map +1 -0
- package/dist/sql.d.ts +26 -0
- package/dist/sql.js +21 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage.d.ts +12 -0
- package/dist/storage.js +40 -0
- package/dist/storage.js.map +1 -0
- package/dist/store.d.ts +53 -0
- package/dist/store.js +100 -0
- package/dist/store.js.map +1 -0
- package/dist/transaction.d.ts +24 -0
- package/dist/transaction.js +50 -0
- package/dist/transaction.js.map +1 -0
- package/package.json +61 -0
package/dist/sql.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one idiom behind every filtered read in the package: a `where` clause
|
|
3
|
+
* assembled from a list of AND-ed conditions and a positional values array.
|
|
4
|
+
* The journal, the dead-letter surface, and the migration candidate query
|
|
5
|
+
* each read a different table, but they all number their placeholders and
|
|
6
|
+
* join their conditions the same way — stated once here, so a new filter
|
|
7
|
+
* cannot number a placeholder wrong.
|
|
8
|
+
*/
|
|
9
|
+
/** A `where` clause under assembly. */
|
|
10
|
+
export interface SqlWhere {
|
|
11
|
+
/** The AND-ed conditions; push to add one. */
|
|
12
|
+
readonly conditions: string[];
|
|
13
|
+
/** The bound values, positionally matching the `$n` placeholders. */
|
|
14
|
+
readonly values: unknown[];
|
|
15
|
+
/** Append a value and return its `$n` placeholder. */
|
|
16
|
+
readonly bind: (value: unknown) => string;
|
|
17
|
+
/** The conditions joined with ` and ` — the body of the `where` clause. */
|
|
18
|
+
readonly where: () => string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Start a `where` clause. Fixed conditions and their values are the seed —
|
|
22
|
+
* a seeded condition may spell its own `$n` as long as it matches the
|
|
23
|
+
* value's position — and optional conditions are pushed with placeholders
|
|
24
|
+
* from `bind`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const sqlWhere: (conditions?: string[], values?: unknown[]) => SqlWhere;
|
package/dist/sql.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one idiom behind every filtered read in the package: a `where` clause
|
|
3
|
+
* assembled from a list of AND-ed conditions and a positional values array.
|
|
4
|
+
* The journal, the dead-letter surface, and the migration candidate query
|
|
5
|
+
* each read a different table, but they all number their placeholders and
|
|
6
|
+
* join their conditions the same way — stated once here, so a new filter
|
|
7
|
+
* cannot number a placeholder wrong.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Start a `where` clause. Fixed conditions and their values are the seed —
|
|
11
|
+
* a seeded condition may spell its own `$n` as long as it matches the
|
|
12
|
+
* value's position — and optional conditions are pushed with placeholders
|
|
13
|
+
* from `bind`.
|
|
14
|
+
*/
|
|
15
|
+
export const sqlWhere = (conditions = [], values = []) => ({
|
|
16
|
+
conditions,
|
|
17
|
+
values,
|
|
18
|
+
bind: (value) => `$${values.push(value)}`,
|
|
19
|
+
where: () => conditions.join(' and '),
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=sql.js.map
|
package/dist/sql.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.js","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,aAAuB,EAAE,EACzB,SAAoB,EAAE,EACZ,EAAE,CAAC,CAAC;IACd,UAAU;IACV,MAAM;IACN,IAAI,EAAE,CAAC,KAAc,EAAU,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;IAC1D,KAAK,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;CACtC,CAAC,CAAA","sourcesContent":["/**\n * The one idiom behind every filtered read in the package: a `where` clause\n * assembled from a list of AND-ed conditions and a positional values array.\n * The journal, the dead-letter surface, and the migration candidate query\n * each read a different table, but they all number their placeholders and\n * join their conditions the same way — stated once here, so a new filter\n * cannot number a placeholder wrong.\n */\n\n/** A `where` clause under assembly. */\nexport interface SqlWhere {\n /** The AND-ed conditions; push to add one. */\n readonly conditions: string[]\n /** The bound values, positionally matching the `$n` placeholders. */\n readonly values: unknown[]\n /** Append a value and return its `$n` placeholder. */\n readonly bind: (value: unknown) => string\n /** The conditions joined with ` and ` — the body of the `where` clause. */\n readonly where: () => string\n}\n\n/**\n * Start a `where` clause. Fixed conditions and their values are the seed —\n * a seeded condition may spell its own `$n` as long as it matches the\n * value's position — and optional conditions are pushed with placeholders\n * from `bind`.\n */\nexport const sqlWhere = (\n conditions: string[] = [],\n values: unknown[] = [],\n): SqlWhere => ({\n conditions,\n values,\n bind: (value: unknown): string => `$${values.push(value)}`,\n where: () => conditions.join(' and '),\n})\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EngineStorage } from '@affordance/core';
|
|
2
|
+
import type { DatabaseAccess, Transaction } from './queryable.js';
|
|
3
|
+
export interface PgStorageOptions<TCommit> {
|
|
4
|
+
readonly db: DatabaseAccess;
|
|
5
|
+
/** Bind application repositories to the framework's open commit transaction. */
|
|
6
|
+
readonly commitContext: (tx: Transaction) => TCommit;
|
|
7
|
+
}
|
|
8
|
+
export declare function createPgStorage<TCommit>(options: PgStorageOptions<TCommit>): EngineStorage<TCommit>;
|
|
9
|
+
export declare function createPgStorage(options: {
|
|
10
|
+
readonly db: DatabaseAccess;
|
|
11
|
+
readonly commitContext?: undefined;
|
|
12
|
+
}): EngineStorage<Transaction>;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { correlationsFor, lookupCorrelation, registerCorrelation, } from './correlation.js';
|
|
2
|
+
import { claimDelivery, readDeadLetters, settle } from './delivery.js';
|
|
3
|
+
import { pgLifecyclePort } from './execution.js';
|
|
4
|
+
import { readJournal } from './journal.js';
|
|
5
|
+
import { listCases } from './listing.js';
|
|
6
|
+
import { findCandidates, hasCompleted } from './migration.js';
|
|
7
|
+
import { queryableOf } from './queryable.js';
|
|
8
|
+
import { insertStoredCase, selectCaseUntyped } from './store.js';
|
|
9
|
+
export function createPgStorage(options) {
|
|
10
|
+
return options.commitContext === undefined
|
|
11
|
+
? bindStorage(options.db, (tx) => tx)
|
|
12
|
+
: bindStorage(options.db, options.commitContext);
|
|
13
|
+
}
|
|
14
|
+
const bindStorage = (db, context) => {
|
|
15
|
+
const q = queryableOf(db);
|
|
16
|
+
return {
|
|
17
|
+
cases: {
|
|
18
|
+
create: (type, state) => insertStoredCase(q, type, state),
|
|
19
|
+
get: (id) => selectCaseUntyped(q, id),
|
|
20
|
+
list: (options) => listCases(q, options),
|
|
21
|
+
},
|
|
22
|
+
execution: pgLifecyclePort(db, context),
|
|
23
|
+
journal: { read: (id, filter) => readJournal(q, id, filter) },
|
|
24
|
+
correlations: {
|
|
25
|
+
register: (registration) => registerCorrelation(q, registration),
|
|
26
|
+
lookup: (system, externalId) => lookupCorrelation(q, system, externalId),
|
|
27
|
+
list: (id, scope) => correlationsFor(q, id, scope),
|
|
28
|
+
},
|
|
29
|
+
deliveries: {
|
|
30
|
+
acquire: (event, key, reopenable) => claimDelivery(q, event, key, reopenable),
|
|
31
|
+
settle: (id, outcome) => settle(q, id, outcome),
|
|
32
|
+
deadLetters: (filter) => readDeadLetters(q, filter),
|
|
33
|
+
},
|
|
34
|
+
migrations: {
|
|
35
|
+
candidates: (type, marker, options, cursor, limit) => findCandidates(q, type, marker, options, cursor, limit),
|
|
36
|
+
hasCompleted: (id, marker) => hasCompleted(q, id, marker),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EngineStorage } from '@affordance/core'\nimport {\n correlationsFor,\n lookupCorrelation,\n registerCorrelation,\n} from './correlation.js'\nimport { claimDelivery, readDeadLetters, settle } from './delivery.js'\nimport { pgLifecyclePort } from './execution.js'\nimport { readJournal } from './journal.js'\nimport { listCases } from './listing.js'\nimport { findCandidates, hasCompleted } from './migration.js'\nimport type { DatabaseAccess, Transaction } from './queryable.js'\nimport { queryableOf } from './queryable.js'\nimport { insertStoredCase, selectCaseUntyped } from './store.js'\n\nexport interface PgStorageOptions<TCommit> {\n readonly db: DatabaseAccess\n /** Bind application repositories to the framework's open commit transaction. */\n readonly commitContext: (tx: Transaction) => TCommit\n}\n\nexport function createPgStorage<TCommit>(\n options: PgStorageOptions<TCommit>,\n): EngineStorage<TCommit>\nexport function createPgStorage(options: {\n readonly db: DatabaseAccess\n readonly commitContext?: undefined\n}): EngineStorage<Transaction>\nexport function createPgStorage<TCommit>(options: {\n readonly db: DatabaseAccess\n readonly commitContext?: (tx: Transaction) => TCommit\n}) {\n return options.commitContext === undefined\n ? bindStorage(options.db, (tx: Transaction) => tx)\n : bindStorage(options.db, options.commitContext)\n}\n\nconst bindStorage = <TCommit>(\n db: DatabaseAccess,\n context: (tx: Transaction) => TCommit,\n): EngineStorage<TCommit> => {\n const q = queryableOf(db)\n return {\n cases: {\n create: (type, state) => insertStoredCase(q, type, state),\n get: (id) => selectCaseUntyped(q, id),\n list: (options) => listCases(q, options),\n },\n execution: pgLifecyclePort(db, context),\n journal: { read: (id, filter) => readJournal(q, id, filter) },\n correlations: {\n register: (registration) => registerCorrelation(q, registration),\n lookup: (system, externalId) => lookupCorrelation(q, system, externalId),\n list: (id, scope) => correlationsFor(q, id, scope),\n },\n deliveries: {\n acquire: (event, key, reopenable) =>\n claimDelivery(q, event, key, reopenable),\n settle: (id, outcome) => settle(q, id, outcome),\n deadLetters: (filter) => readDeadLetters(q, filter),\n },\n migrations: {\n candidates: (type, marker, options, cursor, limit) =>\n findCandidates(q, type, marker, options, cursor, limit),\n hasCompleted: (id, marker) => hasCompleted(q, id, marker),\n },\n }\n}\n"]}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CaseHandle, Dormancy } from '@affordance/core';
|
|
2
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
|
+
import type { Queryable, Transaction } from './queryable.js';
|
|
4
|
+
/** Row shape of `affordance.cases` (int8 arrives as text from the pg driver). */
|
|
5
|
+
export type CaseRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
case_type: string;
|
|
8
|
+
state: unknown;
|
|
9
|
+
seq: string | number;
|
|
10
|
+
ended_at: Date | null;
|
|
11
|
+
created_at: Date;
|
|
12
|
+
updated_at: Date;
|
|
13
|
+
};
|
|
14
|
+
export declare const toHandle: <State>(row: CaseRow, state: State) => CaseHandle<State>;
|
|
15
|
+
/**
|
|
16
|
+
* `createCase` against an explicit {@link Queryable} — the shared-transaction
|
|
17
|
+
* seam. State is stringified explicitly so array-rooted documents are stored
|
|
18
|
+
* as jsonb rather than misread as Postgres arrays.
|
|
19
|
+
*/
|
|
20
|
+
export declare const insertCase: <S extends StandardSchemaV1>(db: Queryable, caseTypeName: string, stateSchema: S, initialState: StandardSchemaV1.InferInput<S>) => Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>>;
|
|
21
|
+
export declare const insertStoredCase: <TState>(db: Queryable, caseTypeName: string, state: TState) => Promise<CaseHandle<TState>>;
|
|
22
|
+
/** `loadCase` against an explicit {@link Queryable} — the shared-transaction seam. */
|
|
23
|
+
export declare const selectCase: <S extends StandardSchemaV1>(db: Queryable, id: string, stateSchema: S) => Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>>;
|
|
24
|
+
/**
|
|
25
|
+
* Load a Case by id with the stored state **unvalidated** (`unknown`).
|
|
26
|
+
*
|
|
27
|
+
* Additive export for the engine: the engine learns which state
|
|
28
|
+
* schema applies only *from* the loaded row — `case_type` names the
|
|
29
|
+
* registered case type — so it must read the row before it can validate.
|
|
30
|
+
* Every other caller should prefer {@link selectCase} / `loadCase`, which
|
|
31
|
+
* validate; whoever consumes this handle owns validating `state` against the
|
|
32
|
+
* type's schema before trusting it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const selectCaseUntyped: (db: Queryable, id: string) => Promise<CaseHandle<unknown>>;
|
|
35
|
+
/**
|
|
36
|
+
* Load a Case by id **for update** — `select … for update`, so the row lock is
|
|
37
|
+
* held until the calling transaction ends.
|
|
38
|
+
*
|
|
39
|
+
* This is the execution lifecycle's serialization point: the claim and the
|
|
40
|
+
* commit both take the case row's lock first, which is what makes the
|
|
41
|
+
* one-in-flight-execution check (and an expired claim's takeover) a decision
|
|
42
|
+
* no two transactions can make concurrently.
|
|
43
|
+
*/
|
|
44
|
+
export declare const selectCaseForUpdate: (tx: Transaction, id: string) => Promise<CaseHandle<unknown>>;
|
|
45
|
+
/** The dormancy transition a committing Execution applies to the case row (`end()` / `reopen()`). */
|
|
46
|
+
/**
|
|
47
|
+
* Write the next Case State, bump `seq`, and apply the Execution's dormancy
|
|
48
|
+
* transition, if any: `'ended'` stamps `ended_at`, `'reopened'` clears it,
|
|
49
|
+
* `null` leaves it exactly as it was. The state document is **not** validated
|
|
50
|
+
* here — the execution lifecycle validates the handler's return against the
|
|
51
|
+
* case type's schema before calling this, and reports a failure there.
|
|
52
|
+
*/
|
|
53
|
+
export declare const updateCaseState: (tx: Transaction, id: string, state: unknown, dormancy?: Dormancy | null) => Promise<CaseHandle<unknown>>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CaseNotFoundError } from '@affordance/core';
|
|
2
|
+
import { mintId, validateAgainstSchema } from '@affordance/core/storage';
|
|
3
|
+
import { FRAMEWORK_SCHEMA } from './bootstrap.js';
|
|
4
|
+
const CASES = `${FRAMEWORK_SCHEMA}.cases`;
|
|
5
|
+
const CASE_COLUMNS = 'id, case_type, state, seq, ended_at, created_at, updated_at';
|
|
6
|
+
export const toHandle = (row, state) => ({
|
|
7
|
+
id: row.id,
|
|
8
|
+
caseTypeName: row.case_type,
|
|
9
|
+
state,
|
|
10
|
+
// Number() is safe: seq counts executions of one human-paced case and will
|
|
11
|
+
// never approach 2^53.
|
|
12
|
+
seq: Number(row.seq),
|
|
13
|
+
endedAt: row.ended_at,
|
|
14
|
+
createdAt: row.created_at,
|
|
15
|
+
updatedAt: row.updated_at,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* `createCase` against an explicit {@link Queryable} — the shared-transaction
|
|
19
|
+
* seam. State is stringified explicitly so array-rooted documents are stored
|
|
20
|
+
* as jsonb rather than misread as Postgres arrays.
|
|
21
|
+
*/
|
|
22
|
+
export const insertCase = async (db, caseTypeName, stateSchema, initialState) => {
|
|
23
|
+
const state = await validateAgainstSchema(stateSchema, initialState, 'initial state');
|
|
24
|
+
return insertStoredCase(db, caseTypeName, state);
|
|
25
|
+
};
|
|
26
|
+
export const insertStoredCase = async (db, caseTypeName, state) => {
|
|
27
|
+
const id = mintId('case');
|
|
28
|
+
const { rows } = await db.query(`insert into ${CASES} (id, case_type, state)
|
|
29
|
+
values ($1, $2, $3::jsonb)
|
|
30
|
+
returning ${CASE_COLUMNS}`, [id, caseTypeName, JSON.stringify(state)]);
|
|
31
|
+
const row = rows[0];
|
|
32
|
+
if (!row)
|
|
33
|
+
throw new Error(`insert into ${CASES} returned no row`);
|
|
34
|
+
return toHandle(row, state);
|
|
35
|
+
};
|
|
36
|
+
/** `loadCase` against an explicit {@link Queryable} — the shared-transaction seam. */
|
|
37
|
+
export const selectCase = async (db, id, stateSchema) => {
|
|
38
|
+
const handle = await selectCaseUntyped(db, id);
|
|
39
|
+
const state = await validateAgainstSchema(stateSchema, handle.state, 'stored state');
|
|
40
|
+
return { ...handle, state };
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Load a Case by id with the stored state **unvalidated** (`unknown`).
|
|
44
|
+
*
|
|
45
|
+
* Additive export for the engine: the engine learns which state
|
|
46
|
+
* schema applies only *from* the loaded row — `case_type` names the
|
|
47
|
+
* registered case type — so it must read the row before it can validate.
|
|
48
|
+
* Every other caller should prefer {@link selectCase} / `loadCase`, which
|
|
49
|
+
* validate; whoever consumes this handle owns validating `state` against the
|
|
50
|
+
* type's schema before trusting it.
|
|
51
|
+
*/
|
|
52
|
+
export const selectCaseUntyped = async (db, id) => {
|
|
53
|
+
const { rows } = await db.query(`select ${CASE_COLUMNS} from ${CASES} where id = $1`, [id]);
|
|
54
|
+
const row = rows[0];
|
|
55
|
+
if (!row)
|
|
56
|
+
throw new CaseNotFoundError(id);
|
|
57
|
+
return toHandle(row, row.state);
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Load a Case by id **for update** — `select … for update`, so the row lock is
|
|
61
|
+
* held until the calling transaction ends.
|
|
62
|
+
*
|
|
63
|
+
* This is the execution lifecycle's serialization point: the claim and the
|
|
64
|
+
* commit both take the case row's lock first, which is what makes the
|
|
65
|
+
* one-in-flight-execution check (and an expired claim's takeover) a decision
|
|
66
|
+
* no two transactions can make concurrently.
|
|
67
|
+
*/
|
|
68
|
+
export const selectCaseForUpdate = async (tx, id) => {
|
|
69
|
+
const { rows } = await tx.query(`select ${CASE_COLUMNS} from ${CASES} where id = $1 for update`, [id]);
|
|
70
|
+
const row = rows[0];
|
|
71
|
+
if (!row)
|
|
72
|
+
throw new CaseNotFoundError(id);
|
|
73
|
+
return toHandle(row, row.state);
|
|
74
|
+
};
|
|
75
|
+
/** The dormancy transition a committing Execution applies to the case row (`end()` / `reopen()`). */
|
|
76
|
+
/**
|
|
77
|
+
* Write the next Case State, bump `seq`, and apply the Execution's dormancy
|
|
78
|
+
* transition, if any: `'ended'` stamps `ended_at`, `'reopened'` clears it,
|
|
79
|
+
* `null` leaves it exactly as it was. The state document is **not** validated
|
|
80
|
+
* here — the execution lifecycle validates the handler's return against the
|
|
81
|
+
* case type's schema before calling this, and reports a failure there.
|
|
82
|
+
*/
|
|
83
|
+
export const updateCaseState = async (tx, id, state, dormancy = null) => {
|
|
84
|
+
const { rows } = await tx.query(`update ${CASES}
|
|
85
|
+
set state = $2::jsonb,
|
|
86
|
+
seq = seq + 1,
|
|
87
|
+
ended_at = case
|
|
88
|
+
when $3::text = 'ended' then now()
|
|
89
|
+
when $3::text = 'reopened' then null
|
|
90
|
+
else ended_at
|
|
91
|
+
end,
|
|
92
|
+
updated_at = now()
|
|
93
|
+
where id = $1
|
|
94
|
+
returning ${CASE_COLUMNS}`, [id, JSON.stringify(state), dormancy]);
|
|
95
|
+
const row = rows[0];
|
|
96
|
+
if (!row)
|
|
97
|
+
throw new CaseNotFoundError(id);
|
|
98
|
+
return toHandle(row, row.state);
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CaseHandle, Dormancy } from '@affordance/core'\nimport { CaseNotFoundError } from '@affordance/core'\nimport { mintId, validateAgainstSchema } from '@affordance/core/storage'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport { FRAMEWORK_SCHEMA } from './bootstrap.js'\nimport type { Queryable, Transaction } from './queryable.js'\n\nconst CASES = `${FRAMEWORK_SCHEMA}.cases`\nconst CASE_COLUMNS =\n 'id, case_type, state, seq, ended_at, created_at, updated_at'\n\n/** Row shape of `affordance.cases` (int8 arrives as text from the pg driver). */\nexport type CaseRow = {\n id: string\n case_type: string\n state: unknown\n seq: string | number\n ended_at: Date | null\n created_at: Date\n updated_at: Date\n}\n\nexport const toHandle = <State>(\n row: CaseRow,\n state: State,\n): CaseHandle<State> => ({\n id: row.id,\n caseTypeName: row.case_type,\n state,\n // Number() is safe: seq counts executions of one human-paced case and will\n // never approach 2^53.\n seq: Number(row.seq),\n endedAt: row.ended_at,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n})\n\n/**\n * `createCase` against an explicit {@link Queryable} — the shared-transaction\n * seam. State is stringified explicitly so array-rooted documents are stored\n * as jsonb rather than misread as Postgres arrays.\n */\nexport const insertCase = async <S extends StandardSchemaV1>(\n db: Queryable,\n caseTypeName: string,\n stateSchema: S,\n initialState: StandardSchemaV1.InferInput<S>,\n): Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>> => {\n const state = await validateAgainstSchema(\n stateSchema,\n initialState,\n 'initial state',\n )\n return insertStoredCase(db, caseTypeName, state)\n}\n\nexport const insertStoredCase = async <TState>(\n db: Queryable,\n caseTypeName: string,\n state: TState,\n): Promise<CaseHandle<TState>> => {\n const id = mintId('case')\n const { rows } = await db.query<CaseRow>(\n `insert into ${CASES} (id, case_type, state)\n values ($1, $2, $3::jsonb)\n returning ${CASE_COLUMNS}`,\n [id, caseTypeName, JSON.stringify(state)],\n )\n const row = rows[0]\n if (!row) throw new Error(`insert into ${CASES} returned no row`)\n return toHandle(row, state)\n}\n\n/** `loadCase` against an explicit {@link Queryable} — the shared-transaction seam. */\nexport const selectCase = async <S extends StandardSchemaV1>(\n db: Queryable,\n id: string,\n stateSchema: S,\n): Promise<CaseHandle<StandardSchemaV1.InferOutput<S>>> => {\n const handle = await selectCaseUntyped(db, id)\n const state = await validateAgainstSchema(\n stateSchema,\n handle.state,\n 'stored state',\n )\n return { ...handle, state }\n}\n\n/**\n * Load a Case by id with the stored state **unvalidated** (`unknown`).\n *\n * Additive export for the engine: the engine learns which state\n * schema applies only *from* the loaded row — `case_type` names the\n * registered case type — so it must read the row before it can validate.\n * Every other caller should prefer {@link selectCase} / `loadCase`, which\n * validate; whoever consumes this handle owns validating `state` against the\n * type's schema before trusting it.\n */\nexport const selectCaseUntyped = async (\n db: Queryable,\n id: string,\n): Promise<CaseHandle<unknown>> => {\n const { rows } = await db.query<CaseRow>(\n `select ${CASE_COLUMNS} from ${CASES} where id = $1`,\n [id],\n )\n const row = rows[0]\n if (!row) throw new CaseNotFoundError(id)\n return toHandle(row, row.state)\n}\n\n/**\n * Load a Case by id **for update** — `select … for update`, so the row lock is\n * held until the calling transaction ends.\n *\n * This is the execution lifecycle's serialization point: the claim and the\n * commit both take the case row's lock first, which is what makes the\n * one-in-flight-execution check (and an expired claim's takeover) a decision\n * no two transactions can make concurrently.\n */\nexport const selectCaseForUpdate = async (\n tx: Transaction,\n id: string,\n): Promise<CaseHandle<unknown>> => {\n const { rows } = await tx.query<CaseRow>(\n `select ${CASE_COLUMNS} from ${CASES} where id = $1 for update`,\n [id],\n )\n const row = rows[0]\n if (!row) throw new CaseNotFoundError(id)\n return toHandle(row, row.state)\n}\n\n/** The dormancy transition a committing Execution applies to the case row (`end()` / `reopen()`). */\n\n/**\n * Write the next Case State, bump `seq`, and apply the Execution's dormancy\n * transition, if any: `'ended'` stamps `ended_at`, `'reopened'` clears it,\n * `null` leaves it exactly as it was. The state document is **not** validated\n * here — the execution lifecycle validates the handler's return against the\n * case type's schema before calling this, and reports a failure there.\n */\nexport const updateCaseState = async (\n tx: Transaction,\n id: string,\n state: unknown,\n dormancy: Dormancy | null = null,\n): Promise<CaseHandle<unknown>> => {\n const { rows } = await tx.query<CaseRow>(\n `update ${CASES}\n set state = $2::jsonb,\n seq = seq + 1,\n ended_at = case\n when $3::text = 'ended' then now()\n when $3::text = 'reopened' then null\n else ended_at\n end,\n updated_at = now()\n where id = $1\n returning ${CASE_COLUMNS}`,\n [id, JSON.stringify(state), dormancy],\n )\n const row = rows[0]\n if (!row) throw new CaseNotFoundError(id)\n return toHandle(row, row.state)\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transaction seam.
|
|
3
|
+
*
|
|
4
|
+
* The claim and the commit are each one short transaction; the handler runs
|
|
5
|
+
* between them, outside both — so no transaction is ever held open
|
|
6
|
+
* across a handler's external calls. `withTransaction` is how the execution
|
|
7
|
+
* lifecycle gets one, and it is also what a handler's `ctx.onCommit` writes
|
|
8
|
+
* ride along inside.
|
|
9
|
+
*/
|
|
10
|
+
import type { DatabaseAccess, Transaction } from './queryable.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` inside a transaction and hand it the handle to use — a checked-out
|
|
13
|
+
* client when the caller brought a pool, the client itself when they brought
|
|
14
|
+
* one (issuing `begin` on a *pool* would put each statement on a different
|
|
15
|
+
* connection, so the distinction is not cosmetic). Which case applies is the
|
|
16
|
+
* caller's declaration — {@link DatabaseAccess} — never sniffed from the
|
|
17
|
+
* object.
|
|
18
|
+
*
|
|
19
|
+
* Commits on return, rolls back on throw, and always releases what it
|
|
20
|
+
* checked out. A rollback that itself fails is swallowed: the original error
|
|
21
|
+
* is what the caller needs, and a connection too broken to roll back is
|
|
22
|
+
* discarded by the pool anyway.
|
|
23
|
+
*/
|
|
24
|
+
export declare const withTransaction: <T>(db: DatabaseAccess, fn: (tx: Transaction) => Promise<T>) => Promise<T>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transaction seam.
|
|
3
|
+
*
|
|
4
|
+
* The claim and the commit are each one short transaction; the handler runs
|
|
5
|
+
* between them, outside both — so no transaction is ever held open
|
|
6
|
+
* across a handler's external calls. `withTransaction` is how the execution
|
|
7
|
+
* lifecycle gets one, and it is also what a handler's `ctx.onCommit` writes
|
|
8
|
+
* ride along inside.
|
|
9
|
+
*/
|
|
10
|
+
import { withClient } from './queryable.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` inside a transaction and hand it the handle to use — a checked-out
|
|
13
|
+
* client when the caller brought a pool, the client itself when they brought
|
|
14
|
+
* one (issuing `begin` on a *pool* would put each statement on a different
|
|
15
|
+
* connection, so the distinction is not cosmetic). Which case applies is the
|
|
16
|
+
* caller's declaration — {@link DatabaseAccess} — never sniffed from the
|
|
17
|
+
* object.
|
|
18
|
+
*
|
|
19
|
+
* Commits on return, rolls back on throw, and always releases what it
|
|
20
|
+
* checked out. A rollback that itself fails is swallowed: the original error
|
|
21
|
+
* is what the caller needs, and a connection too broken to roll back is
|
|
22
|
+
* discarded by the pool anyway.
|
|
23
|
+
*/
|
|
24
|
+
export const withTransaction = async (db, fn) => {
|
|
25
|
+
if (!('pool' in db))
|
|
26
|
+
return withClient(db.client, () => runTransaction(db.client, fn));
|
|
27
|
+
const client = await db.pool.connect();
|
|
28
|
+
try {
|
|
29
|
+
return await runTransaction(client, fn);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
client.release();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const runTransaction = async (handle, fn) => {
|
|
36
|
+
// The one place the brand is applied: past the `begin` below, this handle
|
|
37
|
+
// really is inside a transaction.
|
|
38
|
+
const tx = handle;
|
|
39
|
+
await tx.query('begin');
|
|
40
|
+
try {
|
|
41
|
+
const result = await fn(tx);
|
|
42
|
+
await tx.query('commit');
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
await tx.query('rollback').catch(() => undefined);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,EAAkB,EAClB,EAAmC,EACvB,EAAE;IACd,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;QACjB,OAAO,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAA;IACnE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAA;IACtC,IAAI,CAAC;QACH,OAAO,MAAM,cAAc,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzC,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,MAAM,cAAc,GAAG,KAAK,EAC1B,MAAiB,EACjB,EAAmC,EACvB,EAAE;IACd,0EAA0E;IAC1E,kCAAkC;IAClC,MAAM,EAAE,GAAG,MAAqB,CAAA;IAChC,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACvB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;QAC3B,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QACxB,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACjD,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA","sourcesContent":["/**\n * The transaction seam.\n *\n * The claim and the commit are each one short transaction; the handler runs\n * between them, outside both — so no transaction is ever held open\n * across a handler's external calls. `withTransaction` is how the execution\n * lifecycle gets one, and it is also what a handler's `ctx.onCommit` writes\n * ride along inside.\n */\n\nimport type { DatabaseAccess, Queryable, Transaction } from './queryable.js'\nimport { withClient } from './queryable.js'\n\n/**\n * Run `fn` inside a transaction and hand it the handle to use — a checked-out\n * client when the caller brought a pool, the client itself when they brought\n * one (issuing `begin` on a *pool* would put each statement on a different\n * connection, so the distinction is not cosmetic). Which case applies is the\n * caller's declaration — {@link DatabaseAccess} — never sniffed from the\n * object.\n *\n * Commits on return, rolls back on throw, and always releases what it\n * checked out. A rollback that itself fails is swallowed: the original error\n * is what the caller needs, and a connection too broken to roll back is\n * discarded by the pool anyway.\n */\nexport const withTransaction = async <T>(\n db: DatabaseAccess,\n fn: (tx: Transaction) => Promise<T>,\n): Promise<T> => {\n if (!('pool' in db))\n return withClient(db.client, () => runTransaction(db.client, fn))\n const client = await db.pool.connect()\n try {\n return await runTransaction(client, fn)\n } finally {\n client.release()\n }\n}\n\nconst runTransaction = async <T>(\n handle: Queryable,\n fn: (tx: Transaction) => Promise<T>,\n): Promise<T> => {\n // The one place the brand is applied: past the `begin` below, this handle\n // really is inside a transaction.\n const tx = handle as Transaction\n await tx.query('begin')\n try {\n const result = await fn(tx)\n await tx.query('commit')\n return result\n } catch (error) {\n await tx.query('rollback').catch(() => undefined)\n throw error\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@affordance/pg",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"author": "Mochicode LLC (https://mochicode.com)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@standard-schema/spec": "^1.1.0",
|
|
15
|
+
"@types/pg": "^8.11.10",
|
|
16
|
+
"@affordance/core": "0.2.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^24.0.0",
|
|
20
|
+
"typescript": "^5.7.2",
|
|
21
|
+
"vitest": "^3.2.0",
|
|
22
|
+
"pg": "^8.13.1",
|
|
23
|
+
"zod": "^4.4.3",
|
|
24
|
+
"@affordance/testkit": "0.0.0"
|
|
25
|
+
},
|
|
26
|
+
"description": "Postgres storage and transactional execution for Affordance.",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.12"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/mochicodecom/affordance.git",
|
|
40
|
+
"directory": "packages/pg"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/mochicodecom/affordance#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/mochicodecom/affordance/issues"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"affordance",
|
|
48
|
+
"case-management",
|
|
49
|
+
"typescript",
|
|
50
|
+
"postgres"
|
|
51
|
+
],
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"registry": "https://registry.npmjs.org"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"build": "node ../../scripts/build-package.mjs"
|
|
60
|
+
}
|
|
61
|
+
}
|