@things-factory/fact-change 0.0.0-stage → 10.1.145

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 (34) hide show
  1. package/dist-server/index.d.ts +6 -0
  2. package/dist-server/index.js +19 -0
  3. package/dist-server/index.js.map +1 -0
  4. package/dist-server/service/fact-change-rows.d.ts +45 -0
  5. package/dist-server/service/fact-change-rows.js +44 -0
  6. package/dist-server/service/fact-change-rows.js.map +1 -0
  7. package/dist-server/service/fact-change-service.d.ts +17 -0
  8. package/dist-server/service/fact-change-service.js +34 -0
  9. package/dist-server/service/fact-change-service.js.map +1 -0
  10. package/dist-server/service/fact-change-text.d.ts +37 -0
  11. package/dist-server/service/fact-change-text.js +53 -0
  12. package/dist-server/service/fact-change-text.js.map +1 -0
  13. package/dist-server/service/fact-change.d.ts +32 -0
  14. package/dist-server/service/fact-change.js +102 -0
  15. package/dist-server/service/fact-change.js.map +1 -0
  16. package/dist-server/tsconfig.tsbuildinfo +1 -0
  17. package/package.json +29 -4
  18. package/server/index.ts +16 -0
  19. package/server/service/fact-change-rows.ts +87 -0
  20. package/server/service/fact-change-service.ts +48 -0
  21. package/server/service/fact-change-text.ts +75 -0
  22. package/server/service/fact-change.ts +81 -0
  23. package/server/tsconfig.json +10 -0
  24. package/test/fact-change-db.test.ts +90 -0
  25. package/test/fact-change-rows.test.ts +65 -0
  26. package/test/fact-change-text.test.ts +87 -0
  27. package/test/resolve-ts-siblings.mjs +63 -0
  28. package/things-factory.config.js +1 -0
  29. package/translations/en.json +4 -0
  30. package/translations/ja.json +4 -0
  31. package/translations/ko.json +4 -0
  32. package/translations/ms.json +4 -0
  33. package/translations/zh.json +4 -0
  34. package/README.md +0 -3
@@ -0,0 +1,6 @@
1
+ import { FactChange } from './service/fact-change.js';
2
+ export * from './service/fact-change.js';
3
+ export * from './service/fact-change-text.js';
4
+ export * from './service/fact-change-rows.js';
5
+ export * from './service/fact-change-service.js';
6
+ export declare const entities: (typeof FactChange)[];
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.entities = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /*
6
+ * What moved on a recorded fact, field by field (ADR-0124).
7
+ *
8
+ * plant (`fact_changes`) and warehouse (`warehouse_change_records`) each kept the same table, and
9
+ * station asked every product for a function that wrote into it. This package holds the one table, the
10
+ * writer every product and station call, and the helpers that turn two versions of a fact into rows.
11
+ * Products keep their vocabulary (`factKind`, `source`) and their own read queries and privileges.
12
+ */
13
+ const fact_change_js_1 = require("./service/fact-change.js");
14
+ tslib_1.__exportStar(require("./service/fact-change.js"), exports);
15
+ tslib_1.__exportStar(require("./service/fact-change-text.js"), exports);
16
+ tslib_1.__exportStar(require("./service/fact-change-rows.js"), exports);
17
+ tslib_1.__exportStar(require("./service/fact-change-service.js"), exports);
18
+ exports.entities = [fact_change_js_1.FactChange];
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;GAOG;AACH,6DAAqD;AAErD,mEAAwC;AACxC,wEAA6C;AAC7C,wEAA6C;AAC7C,2EAAgD;AAEnC,QAAA,QAAQ,GAAG,CAAC,2BAAU,CAAC,CAAA","sourcesContent":["/*\n * What moved on a recorded fact, field by field (ADR-0124).\n *\n * plant (`fact_changes`) and warehouse (`warehouse_change_records`) each kept the same table, and\n * station asked every product for a function that wrote into it. This package holds the one table, the\n * writer every product and station call, and the helpers that turn two versions of a fact into rows.\n * Products keep their vocabulary (`factKind`, `source`) and their own read queries and privileges.\n */\nimport { FactChange } from './service/fact-change.js'\n\nexport * from './service/fact-change.js'\nexport * from './service/fact-change-text.js'\nexport * from './service/fact-change-rows.js'\nexport * from './service/fact-change-service.js'\n\nexport const entities = [FactChange]\n"]}
@@ -0,0 +1,45 @@
1
+ import { type MaybeTransactional } from '@things-factory/webhook/writing-transaction';
2
+ import type { FieldChange } from './fact-change-text.js';
3
+ /**
4
+ * Which rows a change becomes, and when it may be written — judgement only, no storage, so tests run
5
+ * without a database.
6
+ */
7
+ export interface FactChangeWhat {
8
+ /** Which kind of fact — the product's vocabulary (`job-order`, `station-screen-spec`, …). */
9
+ factKind: string;
10
+ /** The fact, by name. */
11
+ factName: string;
12
+ changes: FieldChange[];
13
+ at?: Date;
14
+ }
15
+ export interface FactChangeBy {
16
+ user?: unknown;
17
+ /** Where the change came from, in the product's own words (plant: `manual` · `twin-command` · `intake`). */
18
+ source?: string;
19
+ /** Why. May be empty: empty means nobody wrote one, not that there was no reason. */
20
+ reason?: string;
21
+ /** The approval it came through. Who approved is kept by worklist; this points there. */
22
+ activityInstanceId?: string;
23
+ }
24
+ export interface FactChangeRow extends FieldChange {
25
+ factKind: string;
26
+ factName: string;
27
+ at: Date;
28
+ source?: string;
29
+ reason?: string;
30
+ activityInstanceId?: string;
31
+ changedBy?: unknown;
32
+ }
33
+ /**
34
+ * The rows to write. No moved field, no row — a history filled with "nothing happened" is a history
35
+ * nobody reads.
36
+ *
37
+ * A change that does not say which fact and which field moved is refused: a row about nothing can never
38
+ * be found again and only inflates the count.
39
+ */
40
+ export declare function factChangeRows(what: FactChangeWhat, by: FactChangeBy, now: Date): FactChangeRow[];
41
+ /**
42
+ * A change row is written in the transaction that makes the change. Committed on its own, it outlives a
43
+ * change that then fails, and the history says something happened that did not.
44
+ */
45
+ export declare function assertFactChangeInTransaction(manager: MaybeTransactional | undefined | null): void;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.factChangeRows = factChangeRows;
4
+ exports.assertFactChangeInTransaction = assertFactChangeInTransaction;
5
+ const refusal_js_1 = require("@things-factory/shell/dist-server/refusal/refusal.js");
6
+ const writing_transaction_1 = require("@things-factory/webhook/writing-transaction");
7
+ const blank = (value) => typeof value !== 'string' || !value.trim();
8
+ /**
9
+ * The rows to write. No moved field, no row — a history filled with "nothing happened" is a history
10
+ * nobody reads.
11
+ *
12
+ * A change that does not say which fact and which field moved is refused: a row about nothing can never
13
+ * be found again and only inflates the count.
14
+ */
15
+ function factChangeRows(what, by, now) {
16
+ if (!what.changes?.length)
17
+ return [];
18
+ if (blank(what.factKind) || blank(what.factName) || what.changes.some(one => blank(one.field))) {
19
+ throw new refusal_js_1.Refusal('change-names-no-fact', { kind: what.factKind ?? '', name: what.factName ?? '' }, 'a change record has to say which fact and which field moved');
20
+ }
21
+ const at = what.at ?? now;
22
+ return what.changes.map(one => ({
23
+ factKind: what.factKind,
24
+ factName: what.factName,
25
+ field: one.field,
26
+ ...(one.fromValue !== undefined ? { fromValue: one.fromValue } : {}),
27
+ ...(one.toValue !== undefined ? { toValue: one.toValue } : {}),
28
+ at,
29
+ ...(by.source ? { source: by.source } : {}),
30
+ ...(by.reason ? { reason: by.reason } : {}),
31
+ ...(by.activityInstanceId ? { activityInstanceId: by.activityInstanceId } : {}),
32
+ ...(by.user ? { changedBy: by.user } : {})
33
+ }));
34
+ }
35
+ /**
36
+ * A change row is written in the transaction that makes the change. Committed on its own, it outlives a
37
+ * change that then fails, and the history says something happened that did not.
38
+ */
39
+ function assertFactChangeInTransaction(manager) {
40
+ if ((0, writing_transaction_1.isWritingTransaction)(manager))
41
+ return;
42
+ throw new refusal_js_1.Refusal('change-record-outside-transaction', {}, 'a change record is written in the transaction that makes the change - on its own it can outlive a change that failed');
43
+ }
44
+ //# sourceMappingURL=fact-change-rows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-change-rows.js","sourceRoot":"","sources":["../../server/service/fact-change-rows.ts"],"names":[],"mappings":";;AA+CA,wCAyBC;AAMD,sEAQC;AAtFD,qFAA8E;AAC9E,qFAA2G;AAqC3G,MAAM,KAAK,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;AAE5E;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,IAAoB,EAAE,EAAgB,EAAE,GAAS;IAC9E,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM;QAAE,OAAO,EAAE,CAAA;IAEpC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QAC/F,MAAM,IAAI,oBAAO,CACf,sBAAsB,EACtB,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,EACxD,6DAA6D,CAC9D,CAAA;IACH,CAAC;IAED,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,IAAI,GAAG,CAAA;IAEzB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,GAAG,CAAC,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,GAAG,CAAC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,EAAE;QACF,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,EAAE,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3C,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;GAGG;AACH,SAAgB,6BAA6B,CAAC,OAA8C;IAC1F,IAAI,IAAA,0CAAoB,EAAC,OAAO,CAAC;QAAE,OAAM;IAEzC,MAAM,IAAI,oBAAO,CACf,mCAAmC,EACnC,EAAE,EACF,sHAAsH,CACvH,CAAA;AACH,CAAC","sourcesContent":["import { Refusal } from '@things-factory/shell/dist-server/refusal/refusal.js'\nimport { isWritingTransaction, type MaybeTransactional } from '@things-factory/webhook/writing-transaction'\n\nimport type { FieldChange } from './fact-change-text.js'\n\n/**\n * Which rows a change becomes, and when it may be written — judgement only, no storage, so tests run\n * without a database.\n */\nexport interface FactChangeWhat {\n /** Which kind of fact — the product's vocabulary (`job-order`, `station-screen-spec`, …). */\n factKind: string\n /** The fact, by name. */\n factName: string\n changes: FieldChange[]\n at?: Date\n}\n\nexport interface FactChangeBy {\n user?: unknown\n /** Where the change came from, in the product's own words (plant: `manual` · `twin-command` · `intake`). */\n source?: string\n /** Why. May be empty: empty means nobody wrote one, not that there was no reason. */\n reason?: string\n /** The approval it came through. Who approved is kept by worklist; this points there. */\n activityInstanceId?: string\n}\n\nexport interface FactChangeRow extends FieldChange {\n factKind: string\n factName: string\n at: Date\n source?: string\n reason?: string\n activityInstanceId?: string\n changedBy?: unknown\n}\n\nconst blank = (value: unknown) => typeof value !== 'string' || !value.trim()\n\n/**\n * The rows to write. No moved field, no row — a history filled with \"nothing happened\" is a history\n * nobody reads.\n *\n * A change that does not say which fact and which field moved is refused: a row about nothing can never\n * be found again and only inflates the count.\n */\nexport function factChangeRows(what: FactChangeWhat, by: FactChangeBy, now: Date): FactChangeRow[] {\n if (!what.changes?.length) return []\n\n if (blank(what.factKind) || blank(what.factName) || what.changes.some(one => blank(one.field))) {\n throw new Refusal(\n 'change-names-no-fact',\n { kind: what.factKind ?? '', name: what.factName ?? '' },\n 'a change record has to say which fact and which field moved'\n )\n }\n\n const at = what.at ?? now\n\n return what.changes.map(one => ({\n factKind: what.factKind,\n factName: what.factName,\n field: one.field,\n ...(one.fromValue !== undefined ? { fromValue: one.fromValue } : {}),\n ...(one.toValue !== undefined ? { toValue: one.toValue } : {}),\n at,\n ...(by.source ? { source: by.source } : {}),\n ...(by.reason ? { reason: by.reason } : {}),\n ...(by.activityInstanceId ? { activityInstanceId: by.activityInstanceId } : {}),\n ...(by.user ? { changedBy: by.user } : {})\n }))\n}\n\n/**\n * A change row is written in the transaction that makes the change. Committed on its own, it outlives a\n * change that then fails, and the history says something happened that did not.\n */\nexport function assertFactChangeInTransaction(manager: MaybeTransactional | undefined | null): void {\n if (isWritingTransaction(manager)) return\n\n throw new Refusal(\n 'change-record-outside-transaction',\n {},\n 'a change record is written in the transaction that makes the change - on its own it can outlive a change that failed'\n )\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import type { EntityManager } from 'typeorm';
2
+ import type { Domain } from '@things-factory/shell';
3
+ import { FactChange } from './fact-change.js';
4
+ import { type FactChangeBy, type FactChangeWhat } from './fact-change-rows.js';
5
+ /**
6
+ * Writes the fields that moved, inside the caller's transaction. Returns how many rows were written;
7
+ * an empty change list writes nothing.
8
+ */
9
+ export declare function recordFactChanges(manager: EntityManager, domain: Domain, what: FactChangeWhat, by?: FactChangeBy): Promise<number>;
10
+ /**
11
+ * What moved on one fact. The order is the reader's: a panel of recent changes reads newest first, a
12
+ * history reads in the order things happened.
13
+ *
14
+ * There is no door here that reads a whole domain at once: the table only grows. Each product opens its
15
+ * own query per kind, under that kind's privilege.
16
+ */
17
+ export declare function readFactChanges(manager: EntityManager, domain: Domain, factKind: string, factName: string, order?: 'newest-first' | 'oldest-first'): Promise<FactChange[]>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordFactChanges = recordFactChanges;
4
+ exports.readFactChanges = readFactChanges;
5
+ const fact_change_js_1 = require("./fact-change.js");
6
+ const fact_change_rows_js_1 = require("./fact-change-rows.js");
7
+ /**
8
+ * Writes the fields that moved, inside the caller's transaction. Returns how many rows were written;
9
+ * an empty change list writes nothing.
10
+ */
11
+ async function recordFactChanges(manager, domain, what, by = {}) {
12
+ (0, fact_change_rows_js_1.assertFactChangeInTransaction)(manager);
13
+ const rows = (0, fact_change_rows_js_1.factChangeRows)(what, by, new Date());
14
+ if (!rows.length)
15
+ return 0;
16
+ await manager.getRepository(fact_change_js_1.FactChange).save(rows.map(row => ({ ...row, domain })));
17
+ return rows.length;
18
+ }
19
+ /**
20
+ * What moved on one fact. The order is the reader's: a panel of recent changes reads newest first, a
21
+ * history reads in the order things happened.
22
+ *
23
+ * There is no door here that reads a whole domain at once: the table only grows. Each product opens its
24
+ * own query per kind, under that kind's privilege.
25
+ */
26
+ async function readFactChanges(manager, domain, factKind, factName, order = 'newest-first') {
27
+ const direction = order === 'newest-first' ? 'DESC' : 'ASC';
28
+ return await manager.getRepository(fact_change_js_1.FactChange).find({
29
+ where: { domain: { id: domain.id }, factKind, factName },
30
+ relations: { changedBy: true },
31
+ order: { at: direction, createdAt: direction }
32
+ });
33
+ }
34
+ //# sourceMappingURL=fact-change-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-change-service.js","sourceRoot":"","sources":["../../server/service/fact-change-service.ts"],"names":[],"mappings":";;AAWA,8CAaC;AASD,0CAcC;AA3CD,qDAA6C;AAC7C,+DAA6H;AAE7H;;;GAGG;AACI,KAAK,UAAU,iBAAiB,CACrC,OAAsB,EACtB,MAAc,EACd,IAAoB,EACpB,KAAmB,EAAE;IAErB,IAAA,mDAA6B,EAAC,OAAc,CAAC,CAAA;IAE7C,MAAM,IAAI,GAAG,IAAA,oCAAc,EAAC,IAAI,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;IACjD,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,CAAA;IAE1B,MAAM,OAAO,CAAC,aAAa,CAAC,2BAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,CAAC,CAAQ,CAAC,CAAA;IAC1F,OAAO,IAAI,CAAC,MAAM,CAAA;AACpB,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,eAAe,CACnC,OAAsB,EACtB,MAAc,EACd,QAAgB,EAChB,QAAgB,EAChB,QAAyC,cAAc;IAEvD,MAAM,SAAS,GAAG,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;IAE3D,OAAO,MAAM,OAAO,CAAC,aAAa,CAAC,2BAAU,CAAC,CAAC,IAAI,CAAC;QAClD,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QACxD,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;QAC9B,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;KAC/C,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type { EntityManager } from 'typeorm'\n\nimport type { Domain } from '@things-factory/shell'\n\nimport { FactChange } from './fact-change.js'\nimport { assertFactChangeInTransaction, factChangeRows, type FactChangeBy, type FactChangeWhat } from './fact-change-rows.js'\n\n/**\n * Writes the fields that moved, inside the caller's transaction. Returns how many rows were written;\n * an empty change list writes nothing.\n */\nexport async function recordFactChanges(\n manager: EntityManager,\n domain: Domain,\n what: FactChangeWhat,\n by: FactChangeBy = {}\n): Promise<number> {\n assertFactChangeInTransaction(manager as any)\n\n const rows = factChangeRows(what, by, new Date())\n if (!rows.length) return 0\n\n await manager.getRepository(FactChange).save(rows.map(row => ({ ...row, domain })) as any)\n return rows.length\n}\n\n/**\n * What moved on one fact. The order is the reader's: a panel of recent changes reads newest first, a\n * history reads in the order things happened.\n *\n * There is no door here that reads a whole domain at once: the table only grows. Each product opens its\n * own query per kind, under that kind's privilege.\n */\nexport async function readFactChanges(\n manager: EntityManager,\n domain: Domain,\n factKind: string,\n factName: string,\n order: 'newest-first' | 'oldest-first' = 'newest-first'\n): Promise<FactChange[]> {\n const direction = order === 'newest-first' ? 'DESC' : 'ASC'\n\n return await manager.getRepository(FactChange).find({\n where: { domain: { id: domain.id }, factKind, factName },\n relations: { changedBy: true },\n order: { at: direction, createdAt: direction }\n })\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Turning field values into the text a change row holds, and picking which fields moved.
3
+ *
4
+ * Values are written as text because fields have different types (numbers, times, arrays, open bags)
5
+ * and the row is for reading, not for computing again — the current value lives on the fact itself.
6
+ * Times are ISO so a reader in another region knows which instant it was; arrays and bags are JSON so
7
+ * a reordering shows as a change.
8
+ *
9
+ * "No value" and "empty value" are different facts. `asText` keeps `undefined` for none, so the column
10
+ * stays empty, and writes `''` only when the value really was an empty string.
11
+ */
12
+ export interface FieldChange {
13
+ field: string;
14
+ /** The value before; `undefined` when there was none — not an empty string. */
15
+ fromValue?: string;
16
+ /** The value after; `undefined` when it was cleared. */
17
+ toValue?: string;
18
+ }
19
+ export declare function asText(value: unknown): string | undefined;
20
+ /**
21
+ * Which of `fields` moved between two versions of a fact.
22
+ *
23
+ * The caller names the fields because some move on every save without anyone changing them
24
+ * (`updatedAt`, stamps); comparing everything would bury the real changes under those.
25
+ */
26
+ export declare function changedFields(before: Record<string, unknown>, after: Record<string, unknown>, fields: readonly string[]): FieldChange[];
27
+ /**
28
+ * What an overwrite changes, for rows rewritten in place (a decision taken again, a policy, a
29
+ * destination). Unlike `changedFields`, an empty string counts as no value here: those forms clear a
30
+ * field by sending `''`, and clearing is not a change from nothing to nothing.
31
+ */
32
+ export declare function fieldChanges<T extends Record<string, unknown>>(before: Partial<T> | null | undefined, after: Partial<T>, fields: readonly (keyof T & string)[]): FieldChange[];
33
+ /**
34
+ * A row as it was, at the moment it is deleted: one change per content column with a value, from that
35
+ * value to nothing. A deletion is reversible only if what it removed stays readable (ADR-0064 ③).
36
+ */
37
+ export declare function deletionChanges(snapshot: Record<string, unknown>): FieldChange[];
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asText = asText;
4
+ exports.changedFields = changedFields;
5
+ exports.fieldChanges = fieldChanges;
6
+ exports.deletionChanges = deletionChanges;
7
+ function asText(value) {
8
+ if (value === undefined || value === null)
9
+ return undefined;
10
+ if (value instanceof Date)
11
+ return value.toISOString();
12
+ if (typeof value === 'object')
13
+ return JSON.stringify(value);
14
+ return String(value);
15
+ }
16
+ /**
17
+ * Which of `fields` moved between two versions of a fact.
18
+ *
19
+ * The caller names the fields because some move on every save without anyone changing them
20
+ * (`updatedAt`, stamps); comparing everything would bury the real changes under those.
21
+ */
22
+ function changedFields(before, after, fields) {
23
+ const moved = [];
24
+ for (const field of fields) {
25
+ const fromValue = asText(before[field]);
26
+ const toValue = asText(after[field]);
27
+ if (fromValue === toValue)
28
+ continue;
29
+ moved.push({ field, ...(fromValue !== undefined ? { fromValue } : {}), ...(toValue !== undefined ? { toValue } : {}) });
30
+ }
31
+ return moved;
32
+ }
33
+ /**
34
+ * What an overwrite changes, for rows rewritten in place (a decision taken again, a policy, a
35
+ * destination). Unlike `changedFields`, an empty string counts as no value here: those forms clear a
36
+ * field by sending `''`, and clearing is not a change from nothing to nothing.
37
+ */
38
+ function fieldChanges(before, after, fields) {
39
+ const text = (value) => asText(value === '' ? undefined : value);
40
+ return fields
41
+ .map(field => ({ field, fromValue: text(before?.[field]), toValue: text(after[field]) }))
42
+ .filter(one => one.fromValue !== one.toValue);
43
+ }
44
+ /**
45
+ * A row as it was, at the moment it is deleted: one change per content column with a value, from that
46
+ * value to nothing. A deletion is reversible only if what it removed stays readable (ADR-0064 ③).
47
+ */
48
+ function deletionChanges(snapshot) {
49
+ return Object.entries(snapshot)
50
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
51
+ .map(([field, value]) => ({ field, fromValue: asText(value) }));
52
+ }
53
+ //# sourceMappingURL=fact-change-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-change-text.js","sourceRoot":"","sources":["../../server/service/fact-change-text.ts"],"names":[],"mappings":";;AAmBA,wBAKC;AAQD,sCAeC;AAOD,oCAUC;AAMD,0CAIC;AAvDD,SAAgB,MAAM,CAAC,KAAc;IACnC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAA;IAC3D,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC3D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;AACtB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAC3B,MAA+B,EAC/B,KAA8B,EAC9B,MAAyB;IAEzB,MAAM,KAAK,GAAkB,EAAE,CAAA;IAE/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QACvC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;QACpC,IAAI,SAAS,KAAK,OAAO;YAAE,SAAQ;QACnC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACzH,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAC1B,MAAqC,EACrC,KAAiB,EACjB,MAAqC;IAErC,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAEzE,OAAO,MAAM;SACV,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SACxF,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,SAAS,KAAK,GAAG,CAAC,OAAO,CAAC,CAAA;AACjD,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAAC,QAAiC;IAC/D,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;SAC5E,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;AACnE,CAAC","sourcesContent":["/**\n * Turning field values into the text a change row holds, and picking which fields moved.\n *\n * Values are written as text because fields have different types (numbers, times, arrays, open bags)\n * and the row is for reading, not for computing again — the current value lives on the fact itself.\n * Times are ISO so a reader in another region knows which instant it was; arrays and bags are JSON so\n * a reordering shows as a change.\n *\n * \"No value\" and \"empty value\" are different facts. `asText` keeps `undefined` for none, so the column\n * stays empty, and writes `''` only when the value really was an empty string.\n */\nexport interface FieldChange {\n field: string\n /** The value before; `undefined` when there was none — not an empty string. */\n fromValue?: string\n /** The value after; `undefined` when it was cleared. */\n toValue?: string\n}\n\nexport function asText(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\n/**\n * Which of `fields` moved between two versions of a fact.\n *\n * The caller names the fields because some move on every save without anyone changing them\n * (`updatedAt`, stamps); comparing everything would bury the real changes under those.\n */\nexport function changedFields(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n fields: readonly string[]\n): FieldChange[] {\n const moved: FieldChange[] = []\n\n for (const field of fields) {\n const fromValue = asText(before[field])\n const toValue = asText(after[field])\n if (fromValue === toValue) continue\n moved.push({ field, ...(fromValue !== undefined ? { fromValue } : {}), ...(toValue !== undefined ? { toValue } : {}) })\n }\n\n return moved\n}\n\n/**\n * What an overwrite changes, for rows rewritten in place (a decision taken again, a policy, a\n * destination). Unlike `changedFields`, an empty string counts as no value here: those forms clear a\n * field by sending `''`, and clearing is not a change from nothing to nothing.\n */\nexport function fieldChanges<T extends Record<string, unknown>>(\n before: Partial<T> | null | undefined,\n after: Partial<T>,\n fields: readonly (keyof T & string)[]\n): FieldChange[] {\n const text = (value: unknown) => asText(value === '' ? undefined : value)\n\n return fields\n .map(field => ({ field, fromValue: text(before?.[field]), toValue: text(after[field]) }))\n .filter(one => one.fromValue !== one.toValue)\n}\n\n/**\n * A row as it was, at the moment it is deleted: one change per content column with a value, from that\n * value to nothing. A deletion is reversible only if what it removed stays readable (ADR-0064 ③).\n */\nexport function deletionChanges(snapshot: Record<string, unknown>): FieldChange[] {\n return Object.entries(snapshot)\n .filter(([, value]) => value !== undefined && value !== null && value !== '')\n .map(([field, value]) => ({ field, fromValue: asText(value) }))\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { User } from '@things-factory/auth-base';
2
+ import { Domain } from '@things-factory/shell';
3
+ /**
4
+ * One field of a recorded fact moved — from what, to what, when, who, and through which approval.
5
+ *
6
+ * The audit columns a fact carries (`updatedAt`, `updater`) answer only who touched it last; three
7
+ * changes a day erase the first two. This table keeps each move.
8
+ *
9
+ * The fact is addressed by `factKind` + `factName` in the product's vocabulary, so one table serves job
10
+ * orders, production reports, station screens and anything else without a table per kind (ADR-0124).
11
+ * Approval details are not copied: `activityInstanceId` points at the worklist record that holds them.
12
+ */
13
+ export declare class FactChange {
14
+ readonly id: string;
15
+ domain?: Domain;
16
+ domainId?: string;
17
+ factKind: string;
18
+ factName: string;
19
+ field: string;
20
+ fromValue?: string;
21
+ toValue?: string;
22
+ at: Date;
23
+ /** Where the change came from, in the product's words. Empty when the product does not say. */
24
+ source?: string;
25
+ /** Why. Empty means nobody wrote one, not that there was no reason. */
26
+ reason?: string;
27
+ activityInstanceId?: string;
28
+ /** Who changed it. Empty when the system did; `source` then says which. */
29
+ changedBy?: User;
30
+ changedById?: string;
31
+ createdAt?: Date;
32
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FactChange = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
6
+ const type_graphql_1 = require("type-graphql");
7
+ const auth_base_1 = require("@things-factory/auth-base");
8
+ const shell_1 = require("@things-factory/shell");
9
+ /**
10
+ * One field of a recorded fact moved — from what, to what, when, who, and through which approval.
11
+ *
12
+ * The audit columns a fact carries (`updatedAt`, `updater`) answer only who touched it last; three
13
+ * changes a day erase the first two. This table keeps each move.
14
+ *
15
+ * The fact is addressed by `factKind` + `factName` in the product's vocabulary, so one table serves job
16
+ * orders, production reports, station screens and anything else without a table per kind (ADR-0124).
17
+ * Approval details are not copied: `activityInstanceId` points at the worklist record that holds them.
18
+ */
19
+ let FactChange = class FactChange {
20
+ };
21
+ exports.FactChange = FactChange;
22
+ tslib_1.__decorate([
23
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
24
+ (0, type_graphql_1.Field)(type => type_graphql_1.ID),
25
+ tslib_1.__metadata("design:type", String)
26
+ ], FactChange.prototype, "id", void 0);
27
+ tslib_1.__decorate([
28
+ (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
29
+ (0, type_graphql_1.Field)(type => shell_1.Domain),
30
+ tslib_1.__metadata("design:type", shell_1.Domain)
31
+ ], FactChange.prototype, "domain", void 0);
32
+ tslib_1.__decorate([
33
+ (0, typeorm_1.RelationId)((change) => change.domain),
34
+ tslib_1.__metadata("design:type", String)
35
+ ], FactChange.prototype, "domainId", void 0);
36
+ tslib_1.__decorate([
37
+ (0, typeorm_1.Column)(),
38
+ (0, type_graphql_1.Field)({ description: 'which kind of fact this change belongs to' }),
39
+ tslib_1.__metadata("design:type", String)
40
+ ], FactChange.prototype, "factKind", void 0);
41
+ tslib_1.__decorate([
42
+ (0, typeorm_1.Column)(),
43
+ (0, type_graphql_1.Field)({ description: 'the fact this change belongs to, by name' }),
44
+ tslib_1.__metadata("design:type", String)
45
+ ], FactChange.prototype, "factName", void 0);
46
+ tslib_1.__decorate([
47
+ (0, typeorm_1.Column)(),
48
+ (0, type_graphql_1.Field)({ description: 'which field moved' }),
49
+ tslib_1.__metadata("design:type", String)
50
+ ], FactChange.prototype, "field", void 0);
51
+ tslib_1.__decorate([
52
+ (0, typeorm_1.Column)({ nullable: true }),
53
+ (0, type_graphql_1.Field)({ nullable: true, description: 'the value before, as text — empty means it had none' }),
54
+ tslib_1.__metadata("design:type", String)
55
+ ], FactChange.prototype, "fromValue", void 0);
56
+ tslib_1.__decorate([
57
+ (0, typeorm_1.Column)({ nullable: true }),
58
+ (0, type_graphql_1.Field)({ nullable: true, description: 'the value after, as text' }),
59
+ tslib_1.__metadata("design:type", String)
60
+ ], FactChange.prototype, "toValue", void 0);
61
+ tslib_1.__decorate([
62
+ (0, typeorm_1.Column)(),
63
+ (0, type_graphql_1.Field)({ description: 'when it moved' }),
64
+ tslib_1.__metadata("design:type", Date
65
+ /** Where the change came from, in the product's words. Empty when the product does not say. */
66
+ )
67
+ ], FactChange.prototype, "at", void 0);
68
+ tslib_1.__decorate([
69
+ (0, typeorm_1.Column)({ type: 'varchar', nullable: true }),
70
+ (0, type_graphql_1.Field)({ nullable: true, description: "where this change came from, in the product's words" }),
71
+ tslib_1.__metadata("design:type", String)
72
+ ], FactChange.prototype, "source", void 0);
73
+ tslib_1.__decorate([
74
+ (0, typeorm_1.Column)({ nullable: true }),
75
+ (0, type_graphql_1.Field)({ nullable: true, description: 'why — may be empty; empty means nobody wrote one' }),
76
+ tslib_1.__metadata("design:type", String)
77
+ ], FactChange.prototype, "reason", void 0);
78
+ tslib_1.__decorate([
79
+ (0, typeorm_1.Column)({ nullable: true }),
80
+ (0, type_graphql_1.Field)({ nullable: true, description: 'the approval this came through' }),
81
+ tslib_1.__metadata("design:type", String)
82
+ ], FactChange.prototype, "activityInstanceId", void 0);
83
+ tslib_1.__decorate([
84
+ (0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
85
+ (0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
86
+ tslib_1.__metadata("design:type", auth_base_1.User)
87
+ ], FactChange.prototype, "changedBy", void 0);
88
+ tslib_1.__decorate([
89
+ (0, typeorm_1.RelationId)((change) => change.changedBy),
90
+ tslib_1.__metadata("design:type", String)
91
+ ], FactChange.prototype, "changedById", void 0);
92
+ tslib_1.__decorate([
93
+ (0, typeorm_1.CreateDateColumn)(),
94
+ (0, type_graphql_1.Field)({ nullable: true }),
95
+ tslib_1.__metadata("design:type", Date)
96
+ ], FactChange.prototype, "createdAt", void 0);
97
+ exports.FactChange = FactChange = tslib_1.__decorate([
98
+ (0, typeorm_1.Entity)({ name: 'fact_changes' }),
99
+ (0, typeorm_1.Index)('ix_fact_changes_0', (change) => [change.domain, change.factKind, change.factName, change.at]),
100
+ (0, type_graphql_1.ObjectType)({ description: 'One recorded change to a fact — what moved, who moved it, and why' })
101
+ ], FactChange);
102
+ //# sourceMappingURL=fact-change.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-change.js","sourceRoot":"","sources":["../../server/service/fact-change.ts"],"names":[],"mappings":"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","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain } from '@things-factory/shell'\n\n/**\n * One field of a recorded fact moved — from what, to what, when, who, and through which approval.\n *\n * The audit columns a fact carries (`updatedAt`, `updater`) answer only who touched it last; three\n * changes a day erase the first two. This table keeps each move.\n *\n * The fact is addressed by `factKind` + `factName` in the product's vocabulary, so one table serves job\n * orders, production reports, station screens and anything else without a table per kind (ADR-0124).\n * Approval details are not copied: `activityInstanceId` points at the worklist record that holds them.\n */\n@Entity({ name: 'fact_changes' })\n@Index('ix_fact_changes_0', (change: FactChange) => [change.domain, change.factKind, change.factName, change.at])\n@ObjectType({ description: 'One recorded change to a fact — what moved, who moved it, and why' })\nexport class FactChange {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((change: FactChange) => change.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'which kind of fact this change belongs to' })\n factKind: string\n\n @Column()\n @Field({ description: 'the fact this change belongs to, by name' })\n factName: string\n\n @Column()\n @Field({ description: 'which field moved' })\n field: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'the value before, as text — empty means it had none' })\n fromValue?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'the value after, as text' })\n toValue?: string\n\n @Column()\n @Field({ description: 'when it moved' })\n at: Date\n\n /** Where the change came from, in the product's words. Empty when the product does not say. */\n @Column({ type: 'varchar', nullable: true })\n @Field({ nullable: true, description: \"where this change came from, in the product's words\" })\n source?: string\n\n /** Why. Empty means nobody wrote one, not that there was no reason. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'why — may be empty; empty means nobody wrote one' })\n reason?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'the approval this came through' })\n activityInstanceId?: string\n\n /** Who changed it. Empty when the system did; `source` then says which. */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n changedBy?: User\n\n @RelationId((change: FactChange) => change.changedBy)\n changedById?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n}\n"]}