@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.
Files changed (93) hide show
  1. package/README.md +88 -0
  2. package/agent-meta/manifest.json +16 -0
  3. package/agent-meta/skills/planning-postgres/SKILL.md +292 -0
  4. package/build/consts.d.ts +45 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +45 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/errors.d.ts +10 -0
  9. package/build/errors.d.ts.map +1 -0
  10. package/build/errors.js +13 -0
  11. package/build/errors.js.map +1 -0
  12. package/build/index.d.ts +9 -0
  13. package/build/index.d.ts.map +1 -0
  14. package/build/index.js +8 -0
  15. package/build/index.js.map +1 -0
  16. package/build/resource.d.ts +32 -0
  17. package/build/resource.d.ts.map +1 -0
  18. package/build/resource.js +94 -0
  19. package/build/resource.js.map +1 -0
  20. package/build/schemas.d.ts +20 -0
  21. package/build/schemas.d.ts.map +1 -0
  22. package/build/schemas.js +64 -0
  23. package/build/schemas.js.map +1 -0
  24. package/build/service.d.ts +16 -0
  25. package/build/service.d.ts.map +1 -0
  26. package/build/service.js +34 -0
  27. package/build/service.js.map +1 -0
  28. package/build/sql.d.ts +61 -0
  29. package/build/sql.d.ts.map +1 -0
  30. package/build/sql.js +90 -0
  31. package/build/sql.js.map +1 -0
  32. package/build/store/bus.d.ts +52 -0
  33. package/build/store/bus.d.ts.map +1 -0
  34. package/build/store/bus.js +167 -0
  35. package/build/store/bus.js.map +1 -0
  36. package/build/store/cards.d.ts +35 -0
  37. package/build/store/cards.d.ts.map +1 -0
  38. package/build/store/cards.js +112 -0
  39. package/build/store/cards.js.map +1 -0
  40. package/build/store/fold.d.ts +56 -0
  41. package/build/store/fold.d.ts.map +1 -0
  42. package/build/store/fold.js +453 -0
  43. package/build/store/fold.js.map +1 -0
  44. package/build/store/index.d.ts +22 -0
  45. package/build/store/index.d.ts.map +1 -0
  46. package/build/store/index.js +206 -0
  47. package/build/store/index.js.map +1 -0
  48. package/build/store/links.d.ts +18 -0
  49. package/build/store/links.d.ts.map +1 -0
  50. package/build/store/links.js +54 -0
  51. package/build/store/links.js.map +1 -0
  52. package/build/store/schemas.d.ts +32 -0
  53. package/build/store/schemas.d.ts.map +1 -0
  54. package/build/store/schemas.js +143 -0
  55. package/build/store/schemas.js.map +1 -0
  56. package/build/store/specs.d.ts +10 -0
  57. package/build/store/specs.d.ts.map +1 -0
  58. package/build/store/specs.js +47 -0
  59. package/build/store/specs.js.map +1 -0
  60. package/build/store/transitions.d.ts +44 -0
  61. package/build/store/transitions.d.ts.map +1 -0
  62. package/build/store/transitions.js +164 -0
  63. package/build/store/transitions.js.map +1 -0
  64. package/build/types.d.ts +72 -0
  65. package/build/types.d.ts.map +1 -0
  66. package/build/types.js +2 -0
  67. package/build/types.js.map +1 -0
  68. package/package.json +56 -0
  69. package/src/consts.ts +54 -0
  70. package/src/errors.ts +15 -0
  71. package/src/index.ts +9 -0
  72. package/src/resource.ts +133 -0
  73. package/src/schemas.ts +84 -0
  74. package/src/service.ts +45 -0
  75. package/src/sql.ts +135 -0
  76. package/src/store/bus.ts +217 -0
  77. package/src/store/cards.ts +153 -0
  78. package/src/store/fold.ts +560 -0
  79. package/src/store/index.ts +237 -0
  80. package/src/store/links.ts +77 -0
  81. package/src/store/schemas.ts +174 -0
  82. package/src/store/specs.ts +72 -0
  83. package/src/store/transitions.ts +221 -0
  84. package/src/types.ts +73 -0
  85. package/tests/bus.spec.ts +142 -0
  86. package/tests/conformance.spec.ts +37 -0
  87. package/tests/context.ts +143 -0
  88. package/tests/fold.spec.ts +213 -0
  89. package/tests/schema.spec.ts +89 -0
  90. package/tests/sql.spec.ts +50 -0
  91. package/tests/sync.spec.ts +61 -0
  92. package/tests/tsconfig.json +17 -0
  93. 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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -0,0 +1,9 @@
1
+ export type * from './types.js'
2
+
3
+ export * from './consts.js'
4
+ export * from './errors.js'
5
+ export * from './schemas.js'
6
+ export * from './resource.js'
7
+ export * from './sql.js'
8
+ export * from './store/index.js'
9
+ export * from './service.js'
@@ -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))