@things-factory/shell 10.1.29 → 10.1.31
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/client/themes/dark.css +2 -1
- package/client/themes/light.css +2 -1
- package/client/themes/theme-base.css +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/refusal/index.d.ts +1 -0
- package/dist-server/refusal/index.js +5 -0
- package/dist-server/refusal/index.js.map +1 -0
- package/dist-server/refusal/refusal.d.ts +70 -0
- package/dist-server/refusal/refusal.js +103 -0
- package/dist-server/refusal/refusal.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/tests/md-sys-slot-owners.test.ts +2 -2
- package/tests/refusal.test.ts +63 -0
- package/tests/theme-file-names.test.ts +90 -0
package/client/themes/dark.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Accent-derived slots read `--brand-<slot>` so a product can set them from its own
|
|
2
|
+
* Accent-derived slots read `--brand-<slot>` so a product can set them from its own accent theme
|
|
3
|
+
* file, named after the product (`plant-accent.css`) - never after a shell theme file.
|
|
3
4
|
* Neutral slots read `--suite-<slot>`, set by the product suite (@things-factory/operato-suite) for the
|
|
4
5
|
* apps that depend on it. Bundle order does not matter for a var(); the fallback is the shell value.
|
|
5
6
|
* Error, shadow and scrim stay literal on purpose - nobody above the platform owns them.
|
package/client/themes/light.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Accent-derived slots read `--brand-<slot>` so a product can set them from its own
|
|
2
|
+
* Accent-derived slots read `--brand-<slot>` so a product can set them from its own accent theme
|
|
3
|
+
* file, named after the product (`plant-accent.css`) - never after a shell theme file.
|
|
3
4
|
* Neutral slots read `--suite-<slot>`, set by the product suite (@things-factory/operato-suite) for the
|
|
4
5
|
* apps that depend on it. Bundle order does not matter for a var(); the fallback is the shell value.
|
|
5
6
|
* Error, shadow and scrim stay literal on purpose - nobody above the platform owns them.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* [넣는 것] 브랜드와 무관하게 항상 성립해야 하는 규칙(테마 스킴, 공통 접근성 기본값 등)
|
|
14
14
|
* [넣지 않는 것] 브랜드 색값.
|
|
15
|
-
* Products set accent colours only, as `--brand-<slot>` in their own
|
|
15
|
+
* Products set accent colours only, as `--brand-<slot>` in their own `<product>-accent.css`. The shell
|
|
16
16
|
* `light.css` / `dark.css` read the 26 accent-derived md-sys slots from those variables, the 17
|
|
17
17
|
* neutral slots from `--suite-<slot>` (the product suite's shared greys), and keep the last 6
|
|
18
18
|
* (error, shadow, scrim) literal. See tests/md-sys-slot-owners.test.ts.
|
package/dist-server/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './middlewares/index.js';
|
|
|
8
8
|
export * from './bootstrap-hooks.js';
|
|
9
9
|
export * from './graphql-local-client.js';
|
|
10
10
|
export * from './service/index.js';
|
|
11
|
+
export * from './refusal/index.js';
|
|
11
12
|
export * from './typeorm/long-text-column.js';
|
|
12
13
|
export * from './typeorm/encrypt-transform.js';
|
|
13
14
|
export * from './typeorm/encrypt-json-transform.js';
|
package/dist-server/index.js
CHANGED
|
@@ -11,6 +11,7 @@ tslib_1.__exportStar(require("./middlewares/index.js"), exports);
|
|
|
11
11
|
tslib_1.__exportStar(require("./bootstrap-hooks.js"), exports);
|
|
12
12
|
tslib_1.__exportStar(require("./graphql-local-client.js"), exports);
|
|
13
13
|
tslib_1.__exportStar(require("./service/index.js"), exports);
|
|
14
|
+
tslib_1.__exportStar(require("./refusal/index.js"), exports);
|
|
14
15
|
tslib_1.__exportStar(require("./typeorm/long-text-column.js"), exports);
|
|
15
16
|
tslib_1.__exportStar(require("./typeorm/encrypt-transform.js"), exports);
|
|
16
17
|
tslib_1.__exportStar(require("./typeorm/encrypt-json-transform.js"), exports);
|
package/dist-server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,gEAAqC;AACrC,4EAAiD;AACjD,qEAA0C;AAC1C,2DAAgC;AAChC,sDAA2B;AAC3B,oEAAyC;AACzC,iEAAsC;AACtC,+DAAoC;AACpC,oEAAyC;AACzC,6DAAkC;AAElC,wEAA6C;AAC7C,yEAA8C;AAC9C,8EAAmD;AACnD,uEAA4C;AAC5C,uEAA4C","sourcesContent":["export * from './migrations/index.js'\nexport * from './initializers/naming-strategy.js'\nexport * from './initializers/database.js'\nexport * from './utils/index.js'\nexport * from './pubsub.js'\nexport * from './pubsub-log-transport.js'\nexport * from './middlewares/index.js'\nexport * from './bootstrap-hooks.js'\nexport * from './graphql-local-client.js'\nexport * from './service/index.js'\n\nexport * from './typeorm/long-text-column.js'\nexport * from './typeorm/encrypt-transform.js'\nexport * from './typeorm/encrypt-json-transform.js'\nexport * from './typeorm/json5-transform.js'\nexport * from './typeorm/round-transform.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,gEAAqC;AACrC,4EAAiD;AACjD,qEAA0C;AAC1C,2DAAgC;AAChC,sDAA2B;AAC3B,oEAAyC;AACzC,iEAAsC;AACtC,+DAAoC;AACpC,oEAAyC;AACzC,6DAAkC;AAClC,6DAAkC;AAElC,wEAA6C;AAC7C,yEAA8C;AAC9C,8EAAmD;AACnD,uEAA4C;AAC5C,uEAA4C","sourcesContent":["export * from './migrations/index.js'\nexport * from './initializers/naming-strategy.js'\nexport * from './initializers/database.js'\nexport * from './utils/index.js'\nexport * from './pubsub.js'\nexport * from './pubsub-log-transport.js'\nexport * from './middlewares/index.js'\nexport * from './bootstrap-hooks.js'\nexport * from './graphql-local-client.js'\nexport * from './service/index.js'\nexport * from './refusal/index.js'\n\nexport * from './typeorm/long-text-column.js'\nexport * from './typeorm/encrypt-transform.js'\nexport * from './typeorm/encrypt-json-transform.js'\nexport * from './typeorm/json5-transform.js'\nexport * from './typeorm/round-transform.js'\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './refusal.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/refusal/index.ts"],"names":[],"mappings":";;;AAAA,uDAA4B","sourcesContent":["export * from './refusal.js'\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { GraphQLError } from 'graphql';
|
|
2
|
+
import type { MiddlewareFn } from 'type-graphql';
|
|
3
|
+
/**
|
|
4
|
+
* **A refusal the server knows and names** (ADR-0054).
|
|
5
|
+
*
|
|
6
|
+
* ── Why a gate does not throw a sentence ──────────────────────────────────
|
|
7
|
+
* A gate that throws an English sentence puts English on a Korean screen. A screen that translates
|
|
8
|
+
* it is then writing the reason itself, and the gate and the screen end up with two accounts of the
|
|
9
|
+
* same refusal. So the gate throws a name and values, and the sentence is made where the request's
|
|
10
|
+
* language is known.
|
|
11
|
+
*
|
|
12
|
+
* ── What gets stored is the name ──────────────────────────────────────────
|
|
13
|
+
* Never write the sentence onto an event or a work row. A sentence differs per reader and the wording
|
|
14
|
+
* changes later; a stored one freezes that day's language. The `code` is the contract, and it travels
|
|
15
|
+
* unchanged to the client (ADR-0042 - no mapping table on the way out).
|
|
16
|
+
*
|
|
17
|
+
* ── Two traps ─────────────────────────────────────────────────────────────
|
|
18
|
+
* ① `t(key, undefined)` loses the locale and answers in English, so `params` is always passed - the
|
|
19
|
+
* spread below makes an object even when a refusal carries none.
|
|
20
|
+
* ② Never pass a **value** to `t` as a key: `:` reads as a namespace separator and cuts `urn:epc:…`
|
|
21
|
+
* apart. Values travel in `params` only.
|
|
22
|
+
*
|
|
23
|
+
* ── Why it lives in the shell ─────────────────────────────────────────────
|
|
24
|
+
* operato-plant wrote this first and operato-warehouse copied it; worklist is the third consumer as
|
|
25
|
+
* the work-door screens move there (ADR-0069 decision 3). Everything it needs is `graphql`,
|
|
26
|
+
* `type-graphql` and the request's `t`, all of which sit at or below the shell, so this is the floor
|
|
27
|
+
* that every consumer already stands on. The translations stay with each module: `refusalKey` names
|
|
28
|
+
* `error.<code>` inside the caller's own namespace, and a module that does not declare the key falls
|
|
29
|
+
* back to the English sentence rather than printing the key.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Refusal extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly params: RefusalParams;
|
|
34
|
+
/**
|
|
35
|
+
* @param code this refusal's name (kebab). It states **the gate's fact**; a screen does not pick its
|
|
36
|
+
* own sentence from this name.
|
|
37
|
+
* @param params the values the sentence is built from.
|
|
38
|
+
* @param english the sentence used when the translation key is missing. An English sentence reads
|
|
39
|
+
* better than the key printed raw.
|
|
40
|
+
*/
|
|
41
|
+
constructor(code: string, params: RefusalParams, english: string);
|
|
42
|
+
}
|
|
43
|
+
/** Values a refusal sentence is built from. Scalars only: they travel in the GraphQL response as they are. */
|
|
44
|
+
export type RefusalParams = Record<string, string | number | boolean | null>;
|
|
45
|
+
export declare function refusalKey(code: string): string;
|
|
46
|
+
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
|
47
|
+
/**
|
|
48
|
+
* **The one place a refusal becomes a sentence.** The resolver wrapper and the worklist callbacks both
|
|
49
|
+
* call this, so the two exits cannot drift apart.
|
|
50
|
+
*
|
|
51
|
+
* Apollo's own `extensions.code` gets `BAD_USER_INPUT`: a refusal is not a server error, and left empty
|
|
52
|
+
* Apollo would fill in `INTERNAL_SERVER_ERROR`. The name rides separately in `extensions.refusal`; its
|
|
53
|
+
* presence is what tells a client the server refused on purpose.
|
|
54
|
+
*/
|
|
55
|
+
export declare function speakRefusal(refusal: Refusal, context: {
|
|
56
|
+
t?: Translate;
|
|
57
|
+
}): GraphQLError;
|
|
58
|
+
/** Rethrow a refusal as a sentence in the caller's language; anything else passes through untouched. */
|
|
59
|
+
export declare function rethrowSpoken(error: unknown, context: {
|
|
60
|
+
t?: Translate;
|
|
61
|
+
}): never;
|
|
62
|
+
/** A resolver class passes through this (`@UseMiddleware(SpeakRefusals)`). */
|
|
63
|
+
export declare const SpeakRefusals: MiddlewareFn<any>;
|
|
64
|
+
/**
|
|
65
|
+
* An activity callback runs inside a worklist mutation, not the module's own resolver, so
|
|
66
|
+
* `SpeakRefusals` never sees what it throws. Declarations wrap their callback in this: the same
|
|
67
|
+
* `speakRefusal`, the other exit.
|
|
68
|
+
*/
|
|
69
|
+
export declare function speaksRefusals(callback: (instance: any, addendum: any, context: any) => Promise<void>): (instance: any, addendum: any, context: any) => Promise<void>;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SpeakRefusals = exports.Refusal = void 0;
|
|
4
|
+
exports.refusalKey = refusalKey;
|
|
5
|
+
exports.speakRefusal = speakRefusal;
|
|
6
|
+
exports.rethrowSpoken = rethrowSpoken;
|
|
7
|
+
exports.speaksRefusals = speaksRefusals;
|
|
8
|
+
const graphql_1 = require("graphql");
|
|
9
|
+
/**
|
|
10
|
+
* **A refusal the server knows and names** (ADR-0054).
|
|
11
|
+
*
|
|
12
|
+
* ── Why a gate does not throw a sentence ──────────────────────────────────
|
|
13
|
+
* A gate that throws an English sentence puts English on a Korean screen. A screen that translates
|
|
14
|
+
* it is then writing the reason itself, and the gate and the screen end up with two accounts of the
|
|
15
|
+
* same refusal. So the gate throws a name and values, and the sentence is made where the request's
|
|
16
|
+
* language is known.
|
|
17
|
+
*
|
|
18
|
+
* ── What gets stored is the name ──────────────────────────────────────────
|
|
19
|
+
* Never write the sentence onto an event or a work row. A sentence differs per reader and the wording
|
|
20
|
+
* changes later; a stored one freezes that day's language. The `code` is the contract, and it travels
|
|
21
|
+
* unchanged to the client (ADR-0042 - no mapping table on the way out).
|
|
22
|
+
*
|
|
23
|
+
* ── Two traps ─────────────────────────────────────────────────────────────
|
|
24
|
+
* ① `t(key, undefined)` loses the locale and answers in English, so `params` is always passed - the
|
|
25
|
+
* spread below makes an object even when a refusal carries none.
|
|
26
|
+
* ② Never pass a **value** to `t` as a key: `:` reads as a namespace separator and cuts `urn:epc:…`
|
|
27
|
+
* apart. Values travel in `params` only.
|
|
28
|
+
*
|
|
29
|
+
* ── Why it lives in the shell ─────────────────────────────────────────────
|
|
30
|
+
* operato-plant wrote this first and operato-warehouse copied it; worklist is the third consumer as
|
|
31
|
+
* the work-door screens move there (ADR-0069 decision 3). Everything it needs is `graphql`,
|
|
32
|
+
* `type-graphql` and the request's `t`, all of which sit at or below the shell, so this is the floor
|
|
33
|
+
* that every consumer already stands on. The translations stay with each module: `refusalKey` names
|
|
34
|
+
* `error.<code>` inside the caller's own namespace, and a module that does not declare the key falls
|
|
35
|
+
* back to the English sentence rather than printing the key.
|
|
36
|
+
*/
|
|
37
|
+
class Refusal extends Error {
|
|
38
|
+
/**
|
|
39
|
+
* @param code this refusal's name (kebab). It states **the gate's fact**; a screen does not pick its
|
|
40
|
+
* own sentence from this name.
|
|
41
|
+
* @param params the values the sentence is built from.
|
|
42
|
+
* @param english the sentence used when the translation key is missing. An English sentence reads
|
|
43
|
+
* better than the key printed raw.
|
|
44
|
+
*/
|
|
45
|
+
constructor(code, params, english) {
|
|
46
|
+
super(english);
|
|
47
|
+
this.name = 'Refusal';
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.params = params;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.Refusal = Refusal;
|
|
53
|
+
function refusalKey(code) {
|
|
54
|
+
return `error.${code}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* **The one place a refusal becomes a sentence.** The resolver wrapper and the worklist callbacks both
|
|
58
|
+
* call this, so the two exits cannot drift apart.
|
|
59
|
+
*
|
|
60
|
+
* Apollo's own `extensions.code` gets `BAD_USER_INPUT`: a refusal is not a server error, and left empty
|
|
61
|
+
* Apollo would fill in `INTERNAL_SERVER_ERROR`. The name rides separately in `extensions.refusal`; its
|
|
62
|
+
* presence is what tells a client the server refused on purpose.
|
|
63
|
+
*/
|
|
64
|
+
function speakRefusal(refusal, context) {
|
|
65
|
+
const sentence = context.t
|
|
66
|
+
? /* Always pass params - `undefined` here loses the locale and answers in English. */
|
|
67
|
+
context.t(refusalKey(refusal.code), { ...refusal.params, defaultValue: refusal.message })
|
|
68
|
+
: refusal.message;
|
|
69
|
+
return new graphql_1.GraphQLError(sentence, {
|
|
70
|
+
originalError: refusal,
|
|
71
|
+
extensions: { code: 'BAD_USER_INPUT', refusal: { code: refusal.code, params: refusal.params } }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Rethrow a refusal as a sentence in the caller's language; anything else passes through untouched. */
|
|
75
|
+
function rethrowSpoken(error, context) {
|
|
76
|
+
throw error instanceof Refusal ? speakRefusal(error, context) : error;
|
|
77
|
+
}
|
|
78
|
+
/** A resolver class passes through this (`@UseMiddleware(SpeakRefusals)`). */
|
|
79
|
+
const SpeakRefusals = async ({ context }, next) => {
|
|
80
|
+
try {
|
|
81
|
+
return await next();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
rethrowSpoken(error, context);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
exports.SpeakRefusals = SpeakRefusals;
|
|
88
|
+
/**
|
|
89
|
+
* An activity callback runs inside a worklist mutation, not the module's own resolver, so
|
|
90
|
+
* `SpeakRefusals` never sees what it throws. Declarations wrap their callback in this: the same
|
|
91
|
+
* `speakRefusal`, the other exit.
|
|
92
|
+
*/
|
|
93
|
+
function speaksRefusals(callback) {
|
|
94
|
+
return async (instance, addendum, context) => {
|
|
95
|
+
try {
|
|
96
|
+
await callback(instance, addendum, context);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
rethrowSpoken(error, context);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=refusal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../server/refusal/refusal.ts"],"names":[],"mappings":";;;AAqDA,gCAEC;AAYD,oCAUC;AAGD,sCAEC;AAgBD,wCAUC;AA5GD,qCAAsC;AAGtC;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAa,OAAQ,SAAQ,KAAK;IAIhC;;;;;;OAMG;IACH,YAAY,IAAY,EAAE,MAAqB,EAAE,OAAe;QAC9D,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,SAAS,CAAA;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF;AAjBD,0BAiBC;AAKD,SAAgB,UAAU,CAAC,IAAY;IACrC,OAAO,SAAS,IAAI,EAAE,CAAA;AACxB,CAAC;AAID;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,OAAgB,EAAE,OAA0B;IACvE,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC;QACxB,CAAC,CAAC,oFAAoF;YACpF,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3F,CAAC,CAAC,OAAO,CAAC,OAAO,CAAA;IAEnB,OAAO,IAAI,sBAAY,CAAC,QAAQ,EAAE;QAChC,aAAa,EAAE,OAAO;QACtB,UAAU,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE;KAChG,CAAC,CAAA;AACJ,CAAC;AAED,wGAAwG;AACxG,SAAgB,aAAa,CAAC,KAAc,EAAE,OAA0B;IACtE,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AACvE,CAAC;AAED,8EAA8E;AACvE,MAAM,aAAa,GAAsB,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE;IAC1E,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAA;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC/B,CAAC;AACH,CAAC,CAAA;AANY,QAAA,aAAa,iBAMzB;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAC5B,QAAuE;IAEvE,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;QAC3C,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAA;AACH,CAAC","sourcesContent":["import { GraphQLError } from 'graphql'\nimport type { MiddlewareFn } from 'type-graphql'\n\n/**\n * **A refusal the server knows and names** (ADR-0054).\n *\n * ── Why a gate does not throw a sentence ──────────────────────────────────\n * A gate that throws an English sentence puts English on a Korean screen. A screen that translates\n * it is then writing the reason itself, and the gate and the screen end up with two accounts of the\n * same refusal. So the gate throws a name and values, and the sentence is made where the request's\n * language is known.\n *\n * ── What gets stored is the name ──────────────────────────────────────────\n * Never write the sentence onto an event or a work row. A sentence differs per reader and the wording\n * changes later; a stored one freezes that day's language. The `code` is the contract, and it travels\n * unchanged to the client (ADR-0042 - no mapping table on the way out).\n *\n * ── Two traps ─────────────────────────────────────────────────────────────\n * ① `t(key, undefined)` loses the locale and answers in English, so `params` is always passed - the\n * spread below makes an object even when a refusal carries none.\n * ② Never pass a **value** to `t` as a key: `:` reads as a namespace separator and cuts `urn:epc:…`\n * apart. Values travel in `params` only.\n *\n * ── Why it lives in the shell ─────────────────────────────────────────────\n * operato-plant wrote this first and operato-warehouse copied it; worklist is the third consumer as\n * the work-door screens move there (ADR-0069 decision 3). Everything it needs is `graphql`,\n * `type-graphql` and the request's `t`, all of which sit at or below the shell, so this is the floor\n * that every consumer already stands on. The translations stay with each module: `refusalKey` names\n * `error.<code>` inside the caller's own namespace, and a module that does not declare the key falls\n * back to the English sentence rather than printing the key.\n */\nexport class Refusal extends Error {\n readonly code: string\n readonly params: RefusalParams\n\n /**\n * @param code this refusal's name (kebab). It states **the gate's fact**; a screen does not pick its\n * own sentence from this name.\n * @param params the values the sentence is built from.\n * @param english the sentence used when the translation key is missing. An English sentence reads\n * better than the key printed raw.\n */\n constructor(code: string, params: RefusalParams, english: string) {\n super(english)\n this.name = 'Refusal'\n this.code = code\n this.params = params\n }\n}\n\n/** Values a refusal sentence is built from. Scalars only: they travel in the GraphQL response as they are. */\nexport type RefusalParams = Record<string, string | number | boolean | null>\n\nexport function refusalKey(code: string): string {\n return `error.${code}`\n}\n\ntype Translate = (key: string, options?: Record<string, unknown>) => string\n\n/**\n * **The one place a refusal becomes a sentence.** The resolver wrapper and the worklist callbacks both\n * call this, so the two exits cannot drift apart.\n *\n * Apollo's own `extensions.code` gets `BAD_USER_INPUT`: a refusal is not a server error, and left empty\n * Apollo would fill in `INTERNAL_SERVER_ERROR`. The name rides separately in `extensions.refusal`; its\n * presence is what tells a client the server refused on purpose.\n */\nexport function speakRefusal(refusal: Refusal, context: { t?: Translate }): GraphQLError {\n const sentence = context.t\n ? /* Always pass params - `undefined` here loses the locale and answers in English. */\n context.t(refusalKey(refusal.code), { ...refusal.params, defaultValue: refusal.message })\n : refusal.message\n\n return new GraphQLError(sentence, {\n originalError: refusal,\n extensions: { code: 'BAD_USER_INPUT', refusal: { code: refusal.code, params: refusal.params } }\n })\n}\n\n/** Rethrow a refusal as a sentence in the caller's language; anything else passes through untouched. */\nexport function rethrowSpoken(error: unknown, context: { t?: Translate }): never {\n throw error instanceof Refusal ? speakRefusal(error, context) : error\n}\n\n/** A resolver class passes through this (`@UseMiddleware(SpeakRefusals)`). */\nexport const SpeakRefusals: MiddlewareFn<any> = async ({ context }, next) => {\n try {\n return await next()\n } catch (error) {\n rethrowSpoken(error, context)\n }\n}\n\n/**\n * An activity callback runs inside a worklist mutation, not the module's own resolver, so\n * `SpeakRefusals` never sees what it throws. Declarations wrap their callback in this: the same\n * `speakRefusal`, the other exit.\n */\nexport function speaksRefusals(\n callback: (instance: any, addendum: any, context: any) => Promise<void>\n): (instance: any, addendum: any, context: any) => Promise<void> {\n return async (instance, addendum, context) => {\n try {\n await callback(instance, addendum, context)\n } catch (error) {\n rethrowSpoken(error, context)\n }\n }\n}\n"]}
|