@owlmeans/planning-postgres 0.1.18-rc.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/README.md +88 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/planning-postgres/SKILL.md +292 -0
- package/build/consts.d.ts +45 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +45 -0
- package/build/consts.js.map +1 -0
- package/build/errors.d.ts +10 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +13 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +8 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +32 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +94 -0
- package/build/resource.js.map +1 -0
- package/build/schemas.d.ts +20 -0
- package/build/schemas.d.ts.map +1 -0
- package/build/schemas.js +64 -0
- package/build/schemas.js.map +1 -0
- package/build/service.d.ts +16 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +34 -0
- package/build/service.js.map +1 -0
- package/build/sql.d.ts +61 -0
- package/build/sql.d.ts.map +1 -0
- package/build/sql.js +90 -0
- package/build/sql.js.map +1 -0
- package/build/store/bus.d.ts +52 -0
- package/build/store/bus.d.ts.map +1 -0
- package/build/store/bus.js +167 -0
- package/build/store/bus.js.map +1 -0
- package/build/store/cards.d.ts +35 -0
- package/build/store/cards.d.ts.map +1 -0
- package/build/store/cards.js +112 -0
- package/build/store/cards.js.map +1 -0
- package/build/store/fold.d.ts +56 -0
- package/build/store/fold.d.ts.map +1 -0
- package/build/store/fold.js +453 -0
- package/build/store/fold.js.map +1 -0
- package/build/store/index.d.ts +22 -0
- package/build/store/index.d.ts.map +1 -0
- package/build/store/index.js +206 -0
- package/build/store/index.js.map +1 -0
- package/build/store/links.d.ts +18 -0
- package/build/store/links.d.ts.map +1 -0
- package/build/store/links.js +54 -0
- package/build/store/links.js.map +1 -0
- package/build/store/schemas.d.ts +32 -0
- package/build/store/schemas.d.ts.map +1 -0
- package/build/store/schemas.js +143 -0
- package/build/store/schemas.js.map +1 -0
- package/build/store/specs.d.ts +10 -0
- package/build/store/specs.d.ts.map +1 -0
- package/build/store/specs.js +47 -0
- package/build/store/specs.js.map +1 -0
- package/build/store/transitions.d.ts +44 -0
- package/build/store/transitions.d.ts.map +1 -0
- package/build/store/transitions.js +164 -0
- package/build/store/transitions.js.map +1 -0
- package/build/types.d.ts +72 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +56 -0
- package/src/consts.ts +54 -0
- package/src/errors.ts +15 -0
- package/src/index.ts +9 -0
- package/src/resource.ts +133 -0
- package/src/schemas.ts +84 -0
- package/src/service.ts +45 -0
- package/src/sql.ts +135 -0
- package/src/store/bus.ts +217 -0
- package/src/store/cards.ts +153 -0
- package/src/store/fold.ts +560 -0
- package/src/store/index.ts +237 -0
- package/src/store/links.ts +77 -0
- package/src/store/schemas.ts +174 -0
- package/src/store/specs.ts +72 -0
- package/src/store/transitions.ts +221 -0
- package/src/types.ts +73 -0
- package/tests/bus.spec.ts +142 -0
- package/tests/conformance.spec.ts +37 -0
- package/tests/context.ts +143 -0
- package/tests/fold.spec.ts +213 -0
- package/tests/schema.spec.ts +89 -0
- package/tests/sql.spec.ts +50 -0
- package/tests/sync.spec.ts +61 -0
- package/tests/tsconfig.json +17 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { CommitState, PlanningError, WorkcardConflict } from '@owlmeans/planning';
|
|
2
|
+
import { cleanRecord, col, insertOf, pgFault, recordOf, UNIQUE_VIOLATION, whereOf } from '../sql.js';
|
|
3
|
+
const clean = (record) => Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
|
|
4
|
+
/** The portable criteria of a transition query — what a resource list reads the log with. */
|
|
5
|
+
export const transitionCriteria = (where) => clean({
|
|
6
|
+
entityId: where.entityId,
|
|
7
|
+
card: where.card,
|
|
8
|
+
project: where.project,
|
|
9
|
+
seq: where.sinceSeq != null ? { $gt: where.sinceSeq } : undefined,
|
|
10
|
+
'commit.state': where.state,
|
|
11
|
+
action: where.action,
|
|
12
|
+
});
|
|
13
|
+
export const transitionOf = (row, sql) => recordOf(row, sql.tables.transition);
|
|
14
|
+
export const readTransition = async (sql, id) => {
|
|
15
|
+
const spec = sql.tables.transition;
|
|
16
|
+
const rows = await sql.runner.query(`SELECT * FROM ${spec.qualified} WHERE ${col(spec, 'id')} = $1`, [id]);
|
|
17
|
+
return rows[0] == null ? null : transitionOf(rows[0], sql);
|
|
18
|
+
};
|
|
19
|
+
export const readTransitionByKey = async (sql, entityId, key) => {
|
|
20
|
+
const spec = sql.tables.transition;
|
|
21
|
+
const rows = await sql.runner.query(`SELECT * FROM ${spec.qualified} WHERE ${col(spec, 'entityId')} = $1 AND ${col(spec, 'key')} = $2`, [entityId, key]);
|
|
22
|
+
return rows[0] == null ? null : transitionOf(rows[0], sql);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The log of one card (or several) in `seq` order, as a fold reads it: this package's own statement,
|
|
26
|
+
* so it runs inside a fold's transaction. `size` bounds it (0 is no limit); `total` tells a bounded
|
|
27
|
+
* read that more remain.
|
|
28
|
+
*/
|
|
29
|
+
export const listTransitions = async (sql, where, size = 0) => {
|
|
30
|
+
const spec = sql.tables.transition;
|
|
31
|
+
const fixed = whereOf(spec, {
|
|
32
|
+
entityId: where.entityId,
|
|
33
|
+
card: where.card,
|
|
34
|
+
project: where.project,
|
|
35
|
+
action: where.action,
|
|
36
|
+
});
|
|
37
|
+
const params = [...fixed.params];
|
|
38
|
+
const clauses = [fixed.text];
|
|
39
|
+
if (where.sinceSeq != null) {
|
|
40
|
+
params.push(where.sinceSeq);
|
|
41
|
+
clauses.push(`${col(spec, 'seq')} > $${params.length}`);
|
|
42
|
+
}
|
|
43
|
+
if (where.state != null) {
|
|
44
|
+
params.push(where.state);
|
|
45
|
+
clauses.push(`(${col(spec, 'commit')}->>'state') = $${params.length}`);
|
|
46
|
+
}
|
|
47
|
+
const limit = size > 0 ? ` LIMIT ${Math.floor(size) + 1}` : '';
|
|
48
|
+
const rows = await sql.runner.query(`SELECT * FROM ${spec.qualified} WHERE ${clauses.join(' AND ')} ORDER BY ${col(spec, 'seq')}, ${col(spec, 'id')}${limit}`, params);
|
|
49
|
+
const items = rows.map(row => transitionOf(row, sql));
|
|
50
|
+
return size > 0 && items.length > size ? { items: items.slice(0, size), total: items.length } : { items, total: items.length };
|
|
51
|
+
};
|
|
52
|
+
export const commitTransition = async (sql, id, commit) => {
|
|
53
|
+
const spec = sql.tables.transition;
|
|
54
|
+
await sql.runner.query(`UPDATE ${spec.qualified} SET ${col(spec, 'commit')} = $2::jsonb WHERE ${col(spec, 'id')} = $1`, [id, JSON.stringify(clean(commit))]);
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Allocate the next seq — a compare-and-set of `head` on the card row, stamping `headAt`. A card
|
|
58
|
+
* whose create has not folded yet has no row to count on: it allocates `max(seq) + 1` from the log
|
|
59
|
+
* and lets the unique `(card, seq)` index refuse a repeat.
|
|
60
|
+
*
|
|
61
|
+
* @throws {WorkcardConflict}
|
|
62
|
+
*/
|
|
63
|
+
export const allocateSeq = async (sql, card, expect, at) => {
|
|
64
|
+
const cards = sql.tables.card;
|
|
65
|
+
const head = col(cards, 'head');
|
|
66
|
+
const seq = col(cards, 'seq');
|
|
67
|
+
const guarded = expect != null;
|
|
68
|
+
const allocated = await sql.runner.query(`UPDATE ${cards.qualified} SET ${head} = COALESCE(${head}, ${seq}) + 1, ${col(cards, 'headAt')} = $2`
|
|
69
|
+
+ ` WHERE ${col(cards, 'id')} = $1${guarded ? ` AND COALESCE(${head}, ${seq}) = $3` : ''} RETURNING ${head} AS head`, guarded ? [card, at, expect] : [card, at]);
|
|
70
|
+
if (allocated[0] != null) {
|
|
71
|
+
return Number(allocated[0].head);
|
|
72
|
+
}
|
|
73
|
+
const existing = await sql.runner.query(`SELECT COALESCE(${head}, ${seq}) AS head FROM ${cards.qualified} WHERE ${col(cards, 'id')} = $1`, [card]);
|
|
74
|
+
if (existing[0] != null) {
|
|
75
|
+
throw new WorkcardConflict(`${card}:expected:${expect}:head:${existing[0].head}`);
|
|
76
|
+
}
|
|
77
|
+
const last = await lastSeq(sql, card);
|
|
78
|
+
if (guarded && expect !== last) {
|
|
79
|
+
throw new WorkcardConflict(`${card}:expected:${expect}:head:${last}`);
|
|
80
|
+
}
|
|
81
|
+
return last + 1;
|
|
82
|
+
};
|
|
83
|
+
export const lastSeq = async (sql, card) => {
|
|
84
|
+
const spec = sql.tables.transition;
|
|
85
|
+
const rows = await sql.runner.query(`SELECT COALESCE(MAX(${col(spec, 'seq')}), 0) AS last FROM ${spec.qualified} WHERE ${col(spec, 'card')} = $1`, [card]);
|
|
86
|
+
return Number(rows[0]?.last ?? 0);
|
|
87
|
+
};
|
|
88
|
+
/** Give an allocation back when its append failed and nothing was allocated after it. */
|
|
89
|
+
const releaseSeq = async (sql, card, seq) => {
|
|
90
|
+
const cards = sql.tables.card;
|
|
91
|
+
await sql.runner.query(`UPDATE ${cards.qualified} SET ${col(cards, 'head')} = $2 - 1 WHERE ${col(cards, 'id')} = $1 AND ${col(cards, 'head')} = $2`, [card, seq]);
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The log port. `append` is idempotent on its id; a lost race on the key releases the allocation
|
|
95
|
+
* and answers `PlanningError('duplicate-key')` (the executor then answers the winner's receipt); a
|
|
96
|
+
* lost race on `(card, seq)` — an allocation the fold gave up for lost and filled with a failed
|
|
97
|
+
* placeholder — re-allocates once and lands after it, then refuses with `WorkcardConflict`.
|
|
98
|
+
*/
|
|
99
|
+
export const makeTransitionPort = (deps) => {
|
|
100
|
+
const insert = async (sql, transition) => {
|
|
101
|
+
const statement = insertOf(sql.tables.transition, transition, { tail: 'RETURNING *' });
|
|
102
|
+
const rows = await sql.runner.query(statement.text, statement.params);
|
|
103
|
+
return transitionOf(rows[0], sql);
|
|
104
|
+
};
|
|
105
|
+
const store = {
|
|
106
|
+
append: async (transition) => {
|
|
107
|
+
const sql = await deps.sql();
|
|
108
|
+
const indexes = await deps.indexes();
|
|
109
|
+
let current = {
|
|
110
|
+
...transition, id: transition.id ?? deps.ids(), commit: transition.commit ?? { state: CommitState.Pending },
|
|
111
|
+
};
|
|
112
|
+
for (let attempt = 0;; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
return await insert(sql, current);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const fault = pgFault(error);
|
|
118
|
+
if (fault.code !== UNIQUE_VIOLATION) {
|
|
119
|
+
await releaseSeq(sql, current.card, current.seq).catch(() => undefined);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
if (fault.constraint === indexes.entityKey) {
|
|
123
|
+
await releaseSeq(sql, current.card, current.seq);
|
|
124
|
+
throw new PlanningError(`duplicate-key:${current.key}`);
|
|
125
|
+
}
|
|
126
|
+
if (fault.constraint === indexes.cardSeq) {
|
|
127
|
+
if (attempt === 0 && current.seq > 1) {
|
|
128
|
+
current = { ...current, seq: await allocateSeq(sql, current.card, null, deps.now()) };
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
throw new WorkcardConflict(`${current.card}:seq:${current.seq}`);
|
|
132
|
+
}
|
|
133
|
+
const existing = await readTransition(sql, current.id);
|
|
134
|
+
if (existing != null) {
|
|
135
|
+
return existing;
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
get: async (id) => await readTransition(await deps.sql(), id),
|
|
142
|
+
byKey: async (entityId, key) => await readTransitionByKey(await deps.sql(), entityId, key),
|
|
143
|
+
list: async (where, opts) => {
|
|
144
|
+
await deps.sql();
|
|
145
|
+
const sort = opts?.sort ?? (typeof where.card === 'string' ? ['seq'] : ['at', 'seq']);
|
|
146
|
+
const listed = await deps.resource().list(transitionCriteria(where), { ...opts, sort });
|
|
147
|
+
return { ...listed, items: listed.items.map(item => cleanRecord(item)) };
|
|
148
|
+
},
|
|
149
|
+
nextSeq: async (card, expect) => await allocateSeq(await deps.sql(), card, expect, deps.now()),
|
|
150
|
+
head: async (card) => {
|
|
151
|
+
const sql = await deps.sql();
|
|
152
|
+
const cards = sql.tables.card;
|
|
153
|
+
const rows = await sql.runner.query(`SELECT COALESCE(${col(cards, 'head')}, ${col(cards, 'seq')}) AS head FROM ${cards.qualified} WHERE ${col(cards, 'id')} = $1`, [card]);
|
|
154
|
+
return rows[0] != null ? Number(rows[0].head) : await lastSeq(sql, card);
|
|
155
|
+
},
|
|
156
|
+
commit: async (id, commit) => { await commitTransition(await deps.sql(), id, commit); },
|
|
157
|
+
purge: async (where) => {
|
|
158
|
+
await deps.sql();
|
|
159
|
+
return await deps.resource().purge(transitionCriteria(where));
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
return store;
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=transitions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transitions.js","sourceRoot":"","sources":["../../src/store/transitions.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { BasicContext } from '@owlmeans/context';
|
|
2
|
+
import type { ProjectionStore, Relationship, RelationshipStore, SchemaStore, ScopedSchemaRecord, SpecificationStore, Transition, TransitionStore, Workcard } from '@owlmeans/planning';
|
|
3
|
+
import type { PostgresResource } from '@owlmeans/postgres-resource';
|
|
4
|
+
import type { BindablePlanningStore, CommitHub, FoldResult, PlanningServiceOptions } from '@owlmeans/server-planning';
|
|
5
|
+
import type { DEFAULT_PLANNING_POSTGRES_LIMITS } from './consts.js';
|
|
6
|
+
export type PlanningPostgresLimits = {
|
|
7
|
+
-readonly [K in keyof typeof DEFAULT_PLANNING_POSTGRES_LIMITS]: number;
|
|
8
|
+
};
|
|
9
|
+
/** The resource aliases the store resolves its four tables by. */
|
|
10
|
+
export interface PlanningPostgresAliases {
|
|
11
|
+
card: string;
|
|
12
|
+
transition: string;
|
|
13
|
+
link: string;
|
|
14
|
+
schema: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PlanningPostgresOptions {
|
|
17
|
+
aliases?: Partial<PlanningPostgresAliases>;
|
|
18
|
+
/**
|
|
19
|
+
* NOTIFY every settled commit and schema write on the store's channel, and LISTEN for other
|
|
20
|
+
* processes' (default `true`). `false` leaves a waiter to its poll and a reader to the revision.
|
|
21
|
+
*/
|
|
22
|
+
bus?: boolean;
|
|
23
|
+
limits?: Partial<PlanningPostgresLimits>;
|
|
24
|
+
/** Card and transition ids. */
|
|
25
|
+
ids?: () => string;
|
|
26
|
+
/** ISO-8601 timestamps. */
|
|
27
|
+
now?: () => string;
|
|
28
|
+
}
|
|
29
|
+
export interface PostgresPlanningStoreOptions extends PlanningPostgresOptions {
|
|
30
|
+
/** The context the four resources are registered on — read at call time, never at construction. */
|
|
31
|
+
context: () => BasicContext<any> | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** A card row: the record, plus the allocation bookkeeping no caller ever sees. */
|
|
34
|
+
export interface PlanningCardRecord extends Workcard {
|
|
35
|
+
/** When `head` last moved — how old a gap in the card's log is. Private to the store. */
|
|
36
|
+
headAt?: string;
|
|
37
|
+
}
|
|
38
|
+
/** A schema row: a record, or the private per-organization revision counter (`kind: 'head'`). */
|
|
39
|
+
export type PlanningSchemaRow = Omit<ScopedSchemaRecord, 'kind'> & {
|
|
40
|
+
kind: ScopedSchemaRecord['kind'] | 'head';
|
|
41
|
+
};
|
|
42
|
+
export interface PlanningCardResource extends PostgresResource<PlanningCardRecord> {
|
|
43
|
+
}
|
|
44
|
+
export interface PlanningTransitionResource extends PostgresResource<Transition> {
|
|
45
|
+
}
|
|
46
|
+
export interface PlanningLinkResource extends PostgresResource<Relationship> {
|
|
47
|
+
}
|
|
48
|
+
export interface PlanningSchemaResource extends PostgresResource<PlanningSchemaRow> {
|
|
49
|
+
}
|
|
50
|
+
export interface PostgresPlanningStore extends BindablePlanningStore {
|
|
51
|
+
transitions: TransitionStore;
|
|
52
|
+
cards: ProjectionStore;
|
|
53
|
+
specs: SpecificationStore;
|
|
54
|
+
links: RelationshipStore;
|
|
55
|
+
commits: CommitHub;
|
|
56
|
+
schemas: SchemaStore;
|
|
57
|
+
/**
|
|
58
|
+
* Fold what is pending for one card — one transaction under its advisory lock, the settled
|
|
59
|
+
* events delivered after the commit.
|
|
60
|
+
*/
|
|
61
|
+
fold: (cardId: string) => Promise<FoldResult>;
|
|
62
|
+
/** Fold every card whose oldest pending row is older than `olderThanMs`. Answers how many. */
|
|
63
|
+
recover: (opts?: {
|
|
64
|
+
olderThanMs?: number;
|
|
65
|
+
limit?: number;
|
|
66
|
+
}) => Promise<number>;
|
|
67
|
+
/** Release the LISTEN connection and stop healing. */
|
|
68
|
+
close: () => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export interface PostgresPlanningServiceOptions extends Omit<PlanningServiceOptions, 'store'>, PlanningPostgresOptions {
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EACV,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,kBAAkB,EAAE,kBAAkB,EACrG,UAAU,EAAE,eAAe,EAAE,QAAQ,EACtC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,KAAK,EACV,qBAAqB,EAAE,SAAS,EAAE,UAAU,EAAE,sBAAsB,EACrE,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,aAAa,CAAA;AAEnE,MAAM,MAAM,sBAAsB,GAAG;IAAE,CAAC,UAAU,CAAC,IAAI,MAAM,OAAO,gCAAgC,GAAG,MAAM;CAAE,CAAA;AAE/G,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAA;IAC1C;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,MAAM,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACxC,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,2BAA2B;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,4BAA6B,SAAQ,uBAAuB;IAC3E,mGAAmG;IACnG,OAAO,EAAE,MAAM,YAAY,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;CAC7C;AAED,mFAAmF;AACnF,MAAM,WAAW,kBAAmB,SAAQ,QAAQ;IAClD,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;CAAE,CAAA;AAEhH,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB,CAAC,kBAAkB,CAAC;CAAI;AACtF,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB,CAAC,UAAU,CAAC;CAAI;AACpF,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB,CAAC,YAAY,CAAC;CAAI;AAChF,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB,CAAC,iBAAiB,CAAC;CAAI;AAEvF,MAAM,WAAW,qBAAsB,SAAQ,qBAAqB;IAClE,WAAW,EAAE,eAAe,CAAA;IAC5B,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE,kBAAkB,CAAA;IACzB,KAAK,EAAE,iBAAiB,CAAA;IACxB,OAAO,EAAE,SAAS,CAAA;IAClB,OAAO,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;IAC7C,8FAA8F;IAC9F,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7E,sDAAsD;IACtD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,8BAA+B,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,CAAC,EAAE,uBAAuB;CAAI"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/planning-postgres",
|
|
3
|
+
"version": "0.1.18-rc.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "A durable Postgres PlanningStore for @owlmeans/server-planning — cards, the transition log, links and data-defined types and flows as four tables, an inline fold under a per-card advisory lock, and a LISTEN/NOTIFY commit bus.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -b",
|
|
9
|
+
"dev": "sleep 12 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
10
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
11
|
+
"test": "bun test ./tests"
|
|
12
|
+
},
|
|
13
|
+
"main": "build/index.js",
|
|
14
|
+
"module": "build/index.js",
|
|
15
|
+
"types": "build/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./build/index.js",
|
|
19
|
+
"require": "./build/index.js",
|
|
20
|
+
"default": "./build/index.js",
|
|
21
|
+
"module": "./build/index.js",
|
|
22
|
+
"types": "./build/index.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@owlmeans/basic-ids": "^0.1.18-rc.34",
|
|
27
|
+
"@owlmeans/context": "^0.1.18-rc.32",
|
|
28
|
+
"@owlmeans/error": "^0.1.18-rc.36",
|
|
29
|
+
"@owlmeans/planning": "^0.1.18-rc.11",
|
|
30
|
+
"@owlmeans/postgres-resource": "^0.1.18-rc.37",
|
|
31
|
+
"@owlmeans/resource": "^0.1.18-rc.36",
|
|
32
|
+
"@owlmeans/server-planning": "^0.1.18-rc.13"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"ajv": "*",
|
|
36
|
+
"ajv-formats": "*",
|
|
37
|
+
"pg": "*"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
41
|
+
"@owlmeans/postgres": "^0.1.18-rc.38",
|
|
42
|
+
"@owlmeans/server-context": "^0.1.18-rc.37",
|
|
43
|
+
"@owlmeans/test": "^0.1.18-rc.32",
|
|
44
|
+
"@owlmeans/test-integration": "^0.1.18-rc.29",
|
|
45
|
+
"@types/bun": "^1.4.0",
|
|
46
|
+
"@types/pg": "^8.20.4",
|
|
47
|
+
"ajv": "^8.17.1",
|
|
48
|
+
"ajv-formats": "^3.0.1",
|
|
49
|
+
"nodemon": "^3.1.14",
|
|
50
|
+
"pg": "^8.22.0",
|
|
51
|
+
"typescript": "^7.0.2"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** The four resource aliases — each is also the physical table name (sanitized). */
|
|
2
|
+
export const RES_PLANNING_CARD = 'planning-card'
|
|
3
|
+
export const RES_PLANNING_TRANSITION = 'planning-transition'
|
|
4
|
+
export const RES_PLANNING_LINK = 'planning-link'
|
|
5
|
+
export const RES_PLANNING_SCHEMA = 'planning-schema'
|
|
6
|
+
|
|
7
|
+
/** The alias the store answers `PlanningStore.alias` with, and the service its placeholders name. */
|
|
8
|
+
export const PLANNING_POSTGRES_STORE = 'planning-postgres'
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_PLANNING_POSTGRES_LIMITS = Object.freeze({
|
|
11
|
+
/** The most log rows one fold transaction reads and applies; the rest follow in the next round. */
|
|
12
|
+
foldBatch: 500,
|
|
13
|
+
/**
|
|
14
|
+
* How long a gap in a card's log — a seq allocated with no row yet — is an append still in
|
|
15
|
+
* flight. Older, it is a lost allocation: the fold writes a failed placeholder and goes on.
|
|
16
|
+
*/
|
|
17
|
+
gapGraceMs: 30_000,
|
|
18
|
+
/** A pending row older than this makes every status read of it a heal: a background fold. */
|
|
19
|
+
healAfterMs: 1_000,
|
|
20
|
+
/** What `recover()` folds by default: cards whose oldest pending row is older than this. */
|
|
21
|
+
recoverAfterMs: 60_000,
|
|
22
|
+
/** `SET LOCAL lock_timeout` of a fold transaction, in milliseconds. */
|
|
23
|
+
lockTimeoutMs: 10_000,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/** How many cards one `recover()` folds by default. */
|
|
27
|
+
export const DEFAULT_RECOVER_LIMIT = 100
|
|
28
|
+
|
|
29
|
+
/** The cause and error of a failed placeholder written over a lost allocation. */
|
|
30
|
+
export const LOST_ALLOCATION = 'lost-allocation'
|
|
31
|
+
|
|
32
|
+
/** Rounds (transactions) one `fold()` runs while a round reports more rows than `foldBatch`. */
|
|
33
|
+
export const MAX_FOLD_ROUNDS = 16
|
|
34
|
+
|
|
35
|
+
/** Passes of prelude and fold one transaction runs — each over one run of consecutive pending rows. */
|
|
36
|
+
export const MAX_FOLD_PASSES = 64
|
|
37
|
+
|
|
38
|
+
/** The private schema row that is an organization's monotonic schema revision. */
|
|
39
|
+
export const SCHEMA_HEAD_KIND = 'head'
|
|
40
|
+
export const SCHEMA_HEAD_KEY = 'head'
|
|
41
|
+
|
|
42
|
+
/** Reconnect backoff of the LISTEN connection, doubling from the first to the last, in milliseconds. */
|
|
43
|
+
export const BUS_BACKOFF: readonly [number, number] = Object.freeze([1_000, 30_000]) as readonly [number, number]
|
|
44
|
+
|
|
45
|
+
/** Postgres refuses a NOTIFY payload of 8000 bytes or more; a frame stays under this. */
|
|
46
|
+
export const NOTIFY_PAYLOAD_MAX = 7_900
|
|
47
|
+
|
|
48
|
+
/** The file a target adds for each resource — what a missing-resource error names. */
|
|
49
|
+
export const PLANNING_RESOURCE_FILES = Object.freeze({
|
|
50
|
+
card: 'src/resources/planning/card.ts',
|
|
51
|
+
transition: 'src/resources/planning/transition.ts',
|
|
52
|
+
link: 'src/resources/planning/link.ts',
|
|
53
|
+
schema: 'src/resources/planning/schema.ts',
|
|
54
|
+
})
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A fault of the Postgres planning store — a missing resource, a fold transaction that could not
|
|
5
|
+
* commit. Messages start with `planning-postgres:`; it declares no HTTP status (a fault answers 500).
|
|
6
|
+
*/
|
|
7
|
+
export class PlanningPostgresError extends ResilientError {
|
|
8
|
+
public static override typeName: string = 'PlanningPostgresError'
|
|
9
|
+
|
|
10
|
+
constructor(message: string = 'error') {
|
|
11
|
+
super(PlanningPostgresError.typeName, `planning-postgres:${message}`)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ResilientError.registerErrorClass(PlanningPostgresError)
|
package/src/index.ts
ADDED
package/src/resource.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Relationship, Transition } from '@owlmeans/planning'
|
|
2
|
+
import { getDeclaration, makePostgresResource, PgIndexMethod, pgIdentifier } from '@owlmeans/postgres-resource'
|
|
3
|
+
import type { PgIndexSpec, PostgresResource } from '@owlmeans/postgres-resource'
|
|
4
|
+
import type { ResourceMaker, ResourceRecord } from '@owlmeans/resource'
|
|
5
|
+
import { RES_PLANNING_CARD, RES_PLANNING_LINK, RES_PLANNING_SCHEMA, RES_PLANNING_TRANSITION } from './consts.js'
|
|
6
|
+
import {
|
|
7
|
+
PlanningCardTableSchema, PlanningLinkTableSchema, PlanningSchemaTableSchema, PlanningTransitionTableSchema,
|
|
8
|
+
} from './schemas.js'
|
|
9
|
+
import type {
|
|
10
|
+
PlanningCardRecord, PlanningCardResource, PlanningLinkResource, PlanningPostgresAliases, PlanningSchemaResource,
|
|
11
|
+
PlanningSchemaRow, PlanningTransitionResource,
|
|
12
|
+
} from './types.js'
|
|
13
|
+
|
|
14
|
+
type IndexList = Array<[suffix: string, spec: PgIndexSpec]>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Declare the indexes a maker owns — each only once. The declaration is keyed by alias at module
|
|
18
|
+
* scope, so a maker that runs twice (a target's maker wrapping this one, a spec) re-declaring an
|
|
19
|
+
* index would make the compiler warn about a duplicate on every boot.
|
|
20
|
+
*/
|
|
21
|
+
const declareIndexes = <T extends ResourceRecord>(resource: PostgresResource<T>, alias: string, indexes: IndexList): void => {
|
|
22
|
+
const table = pgIdentifier(alias)
|
|
23
|
+
const declared = getDeclaration(alias).indexes
|
|
24
|
+
for (const [suffix, spec] of indexes) {
|
|
25
|
+
const name = `${table}_${suffix}`
|
|
26
|
+
if (!declared.some(index => index.name === name)) {
|
|
27
|
+
resource.index(name, spec)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The index names a table carries, by suffix — what the store matches a unique violation on. */
|
|
33
|
+
export const planningIndexName = (alias: string, suffix: string): string => `${pgIdentifier(alias)}_${suffix}`
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `planning-card` — projects, cards and specifications in ONE table, routed by `kind` at the query
|
|
37
|
+
* layer. Indexes follow the reads: a list by kind and type, children in order, membership
|
|
38
|
+
* (`parents`, GIN), a board by status, the intrinsic state by recency, labels (GIN), a code within
|
|
39
|
+
* its parent, and a slot's documents.
|
|
40
|
+
*/
|
|
41
|
+
export const makePlanningCardResource = (
|
|
42
|
+
alias: string = RES_PLANNING_CARD, dbAlias?: string, serviceAlias?: string
|
|
43
|
+
): PlanningCardResource => {
|
|
44
|
+
const resource = makePostgresResource<PlanningCardRecord, PlanningCardResource>(alias, dbAlias, serviceAlias)
|
|
45
|
+
resource.schema = PlanningCardTableSchema
|
|
46
|
+
declareIndexes(resource, alias, [
|
|
47
|
+
['entity_kind_type', { columns: ['entityId', 'kind', 'type'] }],
|
|
48
|
+
['parent_order', { columns: ['parent', 'order'] }],
|
|
49
|
+
['parents', { columns: ['parents'], method: PgIndexMethod.Gin }],
|
|
50
|
+
['parent_status', { columns: ['parent', 'status'] }],
|
|
51
|
+
['entity_intrinsic', { columns: ['entityId', 'intrinsic', 'updatedAt'] }],
|
|
52
|
+
['labels', { columns: ['labels'], method: PgIndexMethod.Gin }],
|
|
53
|
+
['code', { columns: ['entityId', 'parent', 'code'], where: '"code" IS NOT NULL' }],
|
|
54
|
+
['slot', { columns: ['parent', 'category'], where: `"kind" = 'specification'` }],
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
return resource
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `planning-transition` — the append-only log. `(card, seq)` is the arbiter of allocation, a key is
|
|
62
|
+
* unique per organization, a project's history reads by time, and the pending rows by age (what
|
|
63
|
+
* `recover()` scans).
|
|
64
|
+
*/
|
|
65
|
+
export const makePlanningTransitionResource = (
|
|
66
|
+
alias: string = RES_PLANNING_TRANSITION, dbAlias?: string, serviceAlias?: string
|
|
67
|
+
): PlanningTransitionResource => {
|
|
68
|
+
const resource = makePostgresResource<Transition, PlanningTransitionResource>(alias, dbAlias, serviceAlias)
|
|
69
|
+
resource.schema = PlanningTransitionTableSchema
|
|
70
|
+
declareIndexes(resource, alias, [
|
|
71
|
+
['card_seq', { columns: ['card', 'seq'], unique: true }],
|
|
72
|
+
['entity_key', { columns: ['entityId', 'key'], unique: true, where: '"key" IS NOT NULL' }],
|
|
73
|
+
['project_at', { columns: ['project', 'at'] }],
|
|
74
|
+
['pending', { columns: ['at'], where: `("commit"->>'state') = 'pending'` }],
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
return resource
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `planning-link` — one row per `(from, to, type)`, read from either end, by type, and by project. */
|
|
81
|
+
export const makePlanningLinkResource = (
|
|
82
|
+
alias: string = RES_PLANNING_LINK, dbAlias?: string, serviceAlias?: string
|
|
83
|
+
): PlanningLinkResource => {
|
|
84
|
+
const resource = makePostgresResource<Relationship, PlanningLinkResource>(alias, dbAlias, serviceAlias)
|
|
85
|
+
resource.schema = PlanningLinkTableSchema
|
|
86
|
+
declareIndexes(resource, alias, [
|
|
87
|
+
['edge', { columns: ['from', 'to', 'type'], unique: true }],
|
|
88
|
+
['inbound', { columns: ['to', 'type'] }],
|
|
89
|
+
['entity_type', { columns: ['entityId', 'type'] }],
|
|
90
|
+
['project', { columns: ['project'] }],
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
return resource
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `planning-schema` — data-defined types and flows, one row per `(organization, project layer,
|
|
98
|
+
* kind, key)`, plus each organization's private revision row (`kind: 'head'`).
|
|
99
|
+
*/
|
|
100
|
+
export const makePlanningSchemaResource = (
|
|
101
|
+
alias: string = RES_PLANNING_SCHEMA, dbAlias?: string, serviceAlias?: string
|
|
102
|
+
): PlanningSchemaResource => {
|
|
103
|
+
const resource = makePostgresResource<PlanningSchemaRow, PlanningSchemaResource>(alias, dbAlias, serviceAlias)
|
|
104
|
+
resource.schema = PlanningSchemaTableSchema
|
|
105
|
+
declareIndexes(resource, alias, [
|
|
106
|
+
['scope', { unique: true, expression: `"entityId", COALESCE("project", ''), "kind", "key"` }],
|
|
107
|
+
['rev', { columns: ['entityId', 'rev'] }],
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
return resource
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const makePlanningCardPostgres: ResourceMaker<PlanningCardRecord, PlanningCardResource> =
|
|
114
|
+
(dbAlias, serviceAlias) => makePlanningCardResource(RES_PLANNING_CARD, dbAlias, serviceAlias)
|
|
115
|
+
|
|
116
|
+
export const makePlanningTransitionPostgres: ResourceMaker<Transition, PlanningTransitionResource> =
|
|
117
|
+
(dbAlias, serviceAlias) => makePlanningTransitionResource(RES_PLANNING_TRANSITION, dbAlias, serviceAlias)
|
|
118
|
+
|
|
119
|
+
export const makePlanningLinkPostgres: ResourceMaker<Relationship, PlanningLinkResource> =
|
|
120
|
+
(dbAlias, serviceAlias) => makePlanningLinkResource(RES_PLANNING_LINK, dbAlias, serviceAlias)
|
|
121
|
+
|
|
122
|
+
export const makePlanningSchemaPostgres: ResourceMaker<PlanningSchemaRow, PlanningSchemaResource> =
|
|
123
|
+
(dbAlias, serviceAlias) => makePlanningSchemaResource(RES_PLANNING_SCHEMA, dbAlias, serviceAlias)
|
|
124
|
+
|
|
125
|
+
/** The four resources under the aliases a store resolves — what `appendPostgresPlanning` registers. */
|
|
126
|
+
export const makePlanningPostgresResources = (
|
|
127
|
+
aliases: PlanningPostgresAliases, dbAlias?: string, serviceAlias?: string
|
|
128
|
+
): [PlanningCardResource, PlanningTransitionResource, PlanningLinkResource, PlanningSchemaResource] => [
|
|
129
|
+
makePlanningCardResource(aliases.card, dbAlias, serviceAlias),
|
|
130
|
+
makePlanningTransitionResource(aliases.transition, dbAlias, serviceAlias),
|
|
131
|
+
makePlanningLinkResource(aliases.link, dbAlias, serviceAlias),
|
|
132
|
+
makePlanningSchemaResource(aliases.schema, dbAlias, serviceAlias),
|
|
133
|
+
]
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnyWorkcardSchema, PlanningSchemaKind, RelationshipSchema, ScopedSchemaRecordSchema, TransitionSchema,
|
|
3
|
+
} from '@owlmeans/planning'
|
|
4
|
+
import type { Relationship, Transition } from '@owlmeans/planning'
|
|
5
|
+
import type { JSONSchemaType } from 'ajv'
|
|
6
|
+
import { SCHEMA_HEAD_KIND } from './consts.js'
|
|
7
|
+
import type { PlanningCardRecord, PlanningSchemaRow } from './types.js'
|
|
8
|
+
|
|
9
|
+
type Properties = Record<string, Record<string, unknown>>
|
|
10
|
+
|
|
11
|
+
const propertiesOf = (schema: object): Properties =>
|
|
12
|
+
structuredClone((schema as { properties: Properties }).properties)
|
|
13
|
+
|
|
14
|
+
const requiredOf = (schema: object): string[] => [...((schema as { required?: string[] }).required ?? [])]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An ISO-8601 timestamp stays TEXT. The record's `format: 'date-time'` would compile to
|
|
18
|
+
* `timestamptz` and marshal through `Date` — which changes the string a record carries and breaks
|
|
19
|
+
* the lexicographic order every `updatedSince`, `at` sort and gap age relies on.
|
|
20
|
+
*/
|
|
21
|
+
const timestamp = (property: Record<string, unknown>): Record<string, unknown> => {
|
|
22
|
+
const { format: _format, ...rest } = property
|
|
23
|
+
return { ...rest, pg: { type: 'text' } }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const integer = (property: Record<string, unknown>): Record<string, unknown> => ({ ...property, pg: { type: 'integer' } })
|
|
27
|
+
|
|
28
|
+
const tableSchema = <T>(properties: Properties, required: string[]): JSONSchemaType<T> => ({
|
|
29
|
+
type: 'object', properties, required, additionalProperties: false,
|
|
30
|
+
}) as unknown as JSONSchemaType<T>
|
|
31
|
+
|
|
32
|
+
const cardProperties = propertiesOf(AnyWorkcardSchema)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `planning-card`: projects, cards and specifications in one table — derived from
|
|
36
|
+
* `AnyWorkcardSchema`, plus the private `headAt` column. `seq`/`head`/`revision`/`bodyChars` are
|
|
37
|
+
* `integer`, `body` is `text`, every timestamp is `text`.
|
|
38
|
+
*/
|
|
39
|
+
export const PlanningCardTableSchema = tableSchema<PlanningCardRecord>({
|
|
40
|
+
...cardProperties,
|
|
41
|
+
createdAt: timestamp(cardProperties.createdAt),
|
|
42
|
+
updatedAt: timestamp(cardProperties.updatedAt),
|
|
43
|
+
closedAt: timestamp(cardProperties.closedAt),
|
|
44
|
+
seq: integer(cardProperties.seq),
|
|
45
|
+
head: integer(cardProperties.head),
|
|
46
|
+
revision: integer(cardProperties.revision),
|
|
47
|
+
bodyChars: integer(cardProperties.bodyChars),
|
|
48
|
+
body: { ...cardProperties.body, pg: { type: 'text' } },
|
|
49
|
+
headAt: { type: 'string', nullable: true, pg: { type: 'text' } },
|
|
50
|
+
}, requiredOf(AnyWorkcardSchema))
|
|
51
|
+
|
|
52
|
+
const transitionProperties = propertiesOf(TransitionSchema)
|
|
53
|
+
|
|
54
|
+
/** `planning-transition`: the append-only log, derived from `TransitionSchema`. */
|
|
55
|
+
export const PlanningTransitionTableSchema = tableSchema<Transition>({
|
|
56
|
+
...transitionProperties,
|
|
57
|
+
seq: integer(transitionProperties.seq),
|
|
58
|
+
at: timestamp(transitionProperties.at),
|
|
59
|
+
}, requiredOf(TransitionSchema))
|
|
60
|
+
|
|
61
|
+
const linkProperties = propertiesOf(RelationshipSchema)
|
|
62
|
+
|
|
63
|
+
/** `planning-link`: typed edges, derived from `RelationshipSchema`. */
|
|
64
|
+
export const PlanningLinkTableSchema = tableSchema<Relationship>({
|
|
65
|
+
...linkProperties,
|
|
66
|
+
createdAt: timestamp(linkProperties.createdAt),
|
|
67
|
+
}, requiredOf(RelationshipSchema))
|
|
68
|
+
|
|
69
|
+
const schemaProperties = propertiesOf(ScopedSchemaRecordSchema)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `planning-schema`: data-defined types and flows, derived from `ScopedSchemaRecordSchema`. `kind`
|
|
73
|
+
* also admits the private `head` row an organization's revision is counted in; `project` is `text`
|
|
74
|
+
* so the uniqueness expression `COALESCE("project", '')` stays one type.
|
|
75
|
+
*/
|
|
76
|
+
export const PlanningSchemaTableSchema = tableSchema<PlanningSchemaRow>({
|
|
77
|
+
...schemaProperties,
|
|
78
|
+
kind: { type: 'string', enum: [...Object.values(PlanningSchemaKind), SCHEMA_HEAD_KIND] },
|
|
79
|
+
project: { ...schemaProperties.project, pg: { type: 'text' } },
|
|
80
|
+
version: integer(schemaProperties.version),
|
|
81
|
+
rev: integer(schemaProperties.rev),
|
|
82
|
+
createdAt: timestamp(schemaProperties.createdAt),
|
|
83
|
+
updatedAt: timestamp(schemaProperties.updatedAt),
|
|
84
|
+
}, requiredOf(ScopedSchemaRecordSchema))
|