@things-factory/shell 10.1.39 → 10.1.44
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/dist-server/bootstrap-hooks.d.ts +1 -0
- package/dist-server/bootstrap-hooks.js +28 -0
- package/dist-server/bootstrap-hooks.js.map +1 -1
- package/dist-server/service/directive-transaction/transaction.js +11 -3
- package/dist-server/service/directive-transaction/transaction.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/after-commit.d.ts +15 -0
- package/dist-server/utils/after-commit.js +60 -0
- package/dist-server/utils/after-commit.js.map +1 -0
- package/dist-server/utils/index.d.ts +1 -0
- package/dist-server/utils/index.js +1 -0
- package/dist-server/utils/index.js.map +1 -1
- package/package.json +2 -2
- package/tests/after-commit.test.ts +200 -0
- package/tests/undefined-custom-properties.test.ts +133 -0
- package/translations/en.json +3 -1
- package/translations/ja.json +3 -1
- package/translations/ko.json +3 -1
- package/translations/ms.json +3 -1
- package/translations/zh.json +4 -1
|
@@ -49,3 +49,4 @@ export interface BootstrapHookArgs {
|
|
|
49
49
|
export declare function emitHook<K extends keyof BootstrapHookArgs>(name: K, ...args: BootstrapHookArgs[K]): void;
|
|
50
50
|
/** 부트스트랩 훅을 타입 안전하게 구독한다(내부적으로 process.on). 기존 process.on 과 병존. */
|
|
51
51
|
export declare function onHook<K extends keyof BootstrapHookArgs>(name: K, handler: (...args: BootstrapHookArgs[K]) => void): void;
|
|
52
|
+
export declare function inBootTurn<T>(work: () => Promise<T>): Promise<T>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BOOTSTRAP_HOOKS = void 0;
|
|
4
4
|
exports.emitHook = emitHook;
|
|
5
5
|
exports.onHook = onHook;
|
|
6
|
+
exports.inBootTurn = inBootTurn;
|
|
6
7
|
/**
|
|
7
8
|
* 부트스트랩 훅 레지스트리 (P1: 타입드 모듈 매니페스트 — 3단계).
|
|
8
9
|
*
|
|
@@ -36,4 +37,31 @@ function emitHook(name, ...args) {
|
|
|
36
37
|
function onHook(name, handler) {
|
|
37
38
|
process.on(name, handler);
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* **기동의 한 줄** — 기동 때 DB 를 쓰는 일을 모듈을 가로질러 차례로 돌린다.
|
|
42
|
+
*
|
|
43
|
+
* ── 왜 필요한가 (2026-09-24) ──────────────────────────────────────────────
|
|
44
|
+
* `bootstrap-module-start` 는 리스너를 차례로 부르지만 **기다리지 않는다.** 모듈마다 비동기 일을 걸면 실제 일은
|
|
45
|
+
* 겹쳐서 돈다. postgres 는 연결이 여럿이라 괜찮지만 SQLite 는 쿼리 러너가 하나라 두 모듈의 트랜잭션이 섞인다 —
|
|
46
|
+
* ops-master 와 work-shift 의 기준선이 그렇게 서로를 깨뜨렸다(한쪽 「transaction within a transaction」, 다른
|
|
47
|
+
* 쪽 「Transaction is not started yet」). 사업장의 제조법과 교대가 기준선 없이 남았다.
|
|
48
|
+
*
|
|
49
|
+
* ── 어떻게 ────────────────────────────────────────────────────────────────
|
|
50
|
+
* 앞의 일이 끝난 뒤에 다음 일을 시작하는 약속 사슬 하나다. 앞의 일이 실패해도 다음 일은 돈다 — 한 모듈의 실패가
|
|
51
|
+
* 다른 모듈의 기동을 막으면 안 된다. 돌려주는 약속은 **그 일의** 결과와 실패를 그대로 전한다.
|
|
52
|
+
*
|
|
53
|
+
* 사슬은 `globalThis` 에 둔다. 설치본에 shell 이 두 벌 실리면(손복사 · 중복 설치) 모듈 변수는 벌마다 따로라
|
|
54
|
+
* 줄이 둘이 되고, 그러면 겹침이 그대로 남는다.
|
|
55
|
+
*
|
|
56
|
+
* 기동 때 **쓰는** 일만 여기 세운다. 타이머를 켜는 것처럼 곧 돌아오는 일은 줄에 세울 까닭이 없고, 오래 도는 일을
|
|
57
|
+
* 세우면 뒤의 모듈이 그만큼 늦게 선다.
|
|
58
|
+
*/
|
|
59
|
+
const BOOT_TURN = Symbol.for('things-factory.shell.boot-turn');
|
|
60
|
+
function inBootTurn(work) {
|
|
61
|
+
const holder = globalThis;
|
|
62
|
+
const before = holder[BOOT_TURN] ?? Promise.resolve();
|
|
63
|
+
const mine = before.then(work, work);
|
|
64
|
+
holder[BOOT_TURN] = mine.then(() => undefined, () => undefined);
|
|
65
|
+
return mine;
|
|
66
|
+
}
|
|
39
67
|
//# sourceMappingURL=bootstrap-hooks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap-hooks.js","sourceRoot":"","sources":["../server/bootstrap-hooks.ts"],"names":[],"mappings":";;;AAqDA,4BAEC;AAGD,wBAKC;
|
|
1
|
+
{"version":3,"file":"bootstrap-hooks.js","sourceRoot":"","sources":["../server/bootstrap-hooks.ts"],"names":[],"mappings":";;;AAqDA,4BAEC;AAGD,wBAKC;AAuBD,gCAWC;AA9FD;;;;;;;;;;;;GAYG;AAEH,oCAAoC;AACvB,QAAA,eAAe,GAAG;IAC7B,kBAAkB,EAAE,+BAA+B;IACnD,oBAAoB,EAAE,kCAAkC;IACxD,iBAAiB,EAAE,sCAAsC;IACzD,kBAAkB,EAAE,uCAAuC;IAC3D,iBAAiB,EAAE,sCAAsC;IACzD,kBAAkB,EAAE,uCAAuC;IAC3D,WAAW,EAAE,wBAAwB;IACrC,aAAa,EAAE,gBAAgB;CACvB,CAAA;AAyBV,oDAAoD;AACpD,SAAgB,QAAQ,CAAoC,IAAO,EAAE,GAAG,IAA0B;IAChG,CAAC;IAAC,OAAe,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAA;AACvC,CAAC;AAED,mEAAmE;AACnE,SAAgB,MAAM,CACpB,IAAO,EACP,OAAgD;IAEhD,OAAO,CAAC,EAAE,CAAC,IAAW,EAAE,OAAc,CAAC,CAAA;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;AAE9D,SAAgB,UAAU,CAAI,IAAsB;IAClD,MAAM,MAAM,GAAG,UAAqE,CAAA;IACpF,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAA;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAEpC,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,IAAI,CAC3B,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CAChB,CAAA;IAED,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import Koa from 'koa'\nimport Router from 'koa-router'\n\n/**\n * 부트스트랩 훅 레지스트리 (P1: 타입드 모듈 매니페스트 — 3단계).\n *\n * shell 은 부트스트랩 과정에서 `process` EventEmitter 로 일련의 라이프사이클 이벤트를 emit 하고,\n * 각 모듈은 이를 listen 해 라우터/미들웨어/구독을 등록한다. 이 파일은 그 문자열 이벤트명과\n * 페이로드를 한곳에서 타입으로 고정한다.\n *\n * 원칙(하위호환):\n * - 내부는 여전히 동일한 `process.emit/on` 을 사용한다 → 기존 136개 모듈의\n * `process.on('bootstrap-module-*', ...)` 는 그대로 동작한다(무변경).\n * - shell 의 emit 측을 `emitHook` 으로 전환해 이벤트명 오타·페이로드 시그니처를 컴파일 체크한다.\n * - 모듈은 원하면 `onHook` 으로 자발적 전환 가능(강제 아님).\n */\n\n/** 부트스트랩 훅 이름 상수 (오타 방지용 단일 소스). */\nexport const BOOTSTRAP_HOOKS = {\n moduleSubscription: 'bootstrap-module-subscription',\n collectSsoMiddleware: 'bootstrap-collect-sso-middleware',\n globalPublicRoute: 'bootstrap-module-global-public-route',\n globalPrivateRoute: 'bootstrap-module-global-private-route',\n domainPublicRoute: 'bootstrap-module-domain-public-route',\n domainPrivateRoute: 'bootstrap-module-domain-private-route',\n moduleStart: 'bootstrap-module-start',\n clientRebuilt: 'client-rebuilt'\n} as const\n\n/** module-start 훅 페이로드. */\nexport interface BootstrapModuleStartPayload {\n app: Koa\n config: any\n builtSchema: any\n httpServer: any\n}\n\n/**\n * 훅 이벤트명 → emit/on 인자 튜플 매핑.\n * (동적 수집 지점인 middleware/collection 인자는 any 로 둔다.)\n */\nexport interface BootstrapHookArgs {\n 'bootstrap-module-subscription': [app: Koa, subscriptionMiddleware: any]\n 'bootstrap-collect-sso-middleware': [value: null, ssoMiddlewares: any[]]\n 'bootstrap-module-global-public-route': [app: Koa, router: Router]\n 'bootstrap-module-global-private-route': [app: Koa, router: Router]\n 'bootstrap-module-domain-public-route': [app: Koa, router: Router]\n 'bootstrap-module-domain-private-route': [app: Koa, router: Router]\n 'bootstrap-module-start': [payload: BootstrapModuleStartPayload]\n 'client-rebuilt': [app: Koa, outputFileSystem: any]\n}\n\n/** 부트스트랩 훅을 타입 안전하게 emit 한다(내부적으로 process.emit). */\nexport function emitHook<K extends keyof BootstrapHookArgs>(name: K, ...args: BootstrapHookArgs[K]): void {\n ;(process as any).emit(name, ...args)\n}\n\n/** 부트스트랩 훅을 타입 안전하게 구독한다(내부적으로 process.on). 기존 process.on 과 병존. */\nexport function onHook<K extends keyof BootstrapHookArgs>(\n name: K,\n handler: (...args: BootstrapHookArgs[K]) => void\n): void {\n process.on(name as any, handler as any)\n}\n\n/**\n * **기동의 한 줄** — 기동 때 DB 를 쓰는 일을 모듈을 가로질러 차례로 돌린다.\n *\n * ── 왜 필요한가 (2026-09-24) ──────────────────────────────────────────────\n * `bootstrap-module-start` 는 리스너를 차례로 부르지만 **기다리지 않는다.** 모듈마다 비동기 일을 걸면 실제 일은\n * 겹쳐서 돈다. postgres 는 연결이 여럿이라 괜찮지만 SQLite 는 쿼리 러너가 하나라 두 모듈의 트랜잭션이 섞인다 —\n * ops-master 와 work-shift 의 기준선이 그렇게 서로를 깨뜨렸다(한쪽 「transaction within a transaction」, 다른\n * 쪽 「Transaction is not started yet」). 사업장의 제조법과 교대가 기준선 없이 남았다.\n *\n * ── 어떻게 ────────────────────────────────────────────────────────────────\n * 앞의 일이 끝난 뒤에 다음 일을 시작하는 약속 사슬 하나다. 앞의 일이 실패해도 다음 일은 돈다 — 한 모듈의 실패가\n * 다른 모듈의 기동을 막으면 안 된다. 돌려주는 약속은 **그 일의** 결과와 실패를 그대로 전한다.\n *\n * 사슬은 `globalThis` 에 둔다. 설치본에 shell 이 두 벌 실리면(손복사 · 중복 설치) 모듈 변수는 벌마다 따로라\n * 줄이 둘이 되고, 그러면 겹침이 그대로 남는다.\n *\n * 기동 때 **쓰는** 일만 여기 세운다. 타이머를 켜는 것처럼 곧 돌아오는 일은 줄에 세울 까닭이 없고, 오래 도는 일을\n * 세우면 뒤의 모듈이 그만큼 늦게 선다.\n */\nconst BOOT_TURN = Symbol.for('things-factory.shell.boot-turn')\n\nexport function inBootTurn<T>(work: () => Promise<T>): Promise<T> {\n const holder = globalThis as unknown as Record<symbol, Promise<unknown> | undefined>\n const before = holder[BOOT_TURN] ?? Promise.resolve()\n const mine = before.then(work, work)\n\n holder[BOOT_TURN] = mine.then(\n () => undefined,\n () => undefined\n )\n\n return mine\n}\n"]}
|
|
@@ -5,7 +5,9 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const graphql_1 = require("graphql");
|
|
6
6
|
const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
|
|
7
7
|
const utils_1 = require("@graphql-tools/utils");
|
|
8
|
+
const env_1 = require("@things-factory/env");
|
|
8
9
|
const database_js_1 = require("../../initializers/database.js");
|
|
10
|
+
const after_commit_js_1 = require("../../utils/after-commit.js");
|
|
9
11
|
const DIRECTIVE = 'transaction';
|
|
10
12
|
exports.transactionDirectiveTypeDefs = (0, graphql_tag_1.default) `
|
|
11
13
|
directive @${DIRECTIVE} on FIELD_DEFINITION
|
|
@@ -16,17 +18,23 @@ const transactionDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema,
|
|
|
16
18
|
if (transactionDirective) {
|
|
17
19
|
const { resolve = graphql_1.defaultFieldResolver } = fieldConfig;
|
|
18
20
|
fieldConfig.resolve = async function (source, args, context, info) {
|
|
19
|
-
|
|
21
|
+
/*
|
|
22
|
+
* The resolver gets context.state.afterCommit: steps that cannot be undone wait there and run
|
|
23
|
+
* once the transaction has returned, which is after the commit. On a rollback they are dropped.
|
|
24
|
+
* See utils/after-commit.ts (ruling 2026-09-24, ADR-0091 decision 8).
|
|
25
|
+
*/
|
|
26
|
+
return await (0, after_commit_js_1.runTransaction)(work => (0, database_js_1.getDataSource)('tx').transaction(work), async (tx, afterCommit) => {
|
|
20
27
|
/* local-graphql-client로부터 invoke인 경우에는 context.req, context.res 가 없으므로, 빈 오브젝트로 대체해준다. */
|
|
21
28
|
let wrap = context.app.createContext(context.req || {}, context.res || {});
|
|
22
29
|
wrap.state = {
|
|
23
30
|
...context.state,
|
|
24
|
-
tx
|
|
31
|
+
tx,
|
|
32
|
+
afterCommit
|
|
25
33
|
};
|
|
26
34
|
wrap.t = context.t;
|
|
27
35
|
let result = await resolve.call(this, source, args, wrap, info);
|
|
28
36
|
return result;
|
|
29
|
-
});
|
|
37
|
+
}, (message, error) => env_1.logger.error(`${message} (field ${fieldName})`, error));
|
|
30
38
|
};
|
|
31
39
|
return fieldConfig;
|
|
32
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../../server/service/directive-transaction/transaction.ts"],"names":[],"mappings":";;;;AAAA,qCAA6D;AAC7D,sEAA6B;AAE7B,gDAA0E;AAE1E,gEAA8D;
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../../server/service/directive-transaction/transaction.ts"],"names":[],"mappings":";;;;AAAA,qCAA6D;AAC7D,sEAA6B;AAE7B,gDAA0E;AAE1E,6CAA4C;AAE5C,gEAA8D;AAC9D,iEAA4D;AAE5D,MAAM,SAAS,GAAG,aAAa,CAAA;AAElB,QAAA,4BAA4B,GAAG,IAAA,qBAAG,EAAA;iBAC9B,SAAS;GACvB,CAAA;AACI,MAAM,4BAA4B,GAAG,CAAC,MAAqB,EAAE,EAAE,CACpE,IAAA,iBAAS,EAAC,MAAM,EAAE;IAChB,CAAC,kBAAU,CAAC,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE;QACpD,MAAM,oBAAoB,GAAG,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAC9E,IAAI,oBAAoB,EAAE,CAAC;YACzB,MAAM,EAAE,OAAO,GAAG,8BAAoB,EAAE,GAAG,WAAW,CAAA;YAEtD,WAAW,CAAC,OAAO,GAAG,KAAK,WAAW,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;gBAC/D;;;;mBAIG;gBACH,OAAO,MAAM,IAAA,gCAAc,EACzB,IAAI,CAAC,EAAE,CAAC,IAAA,2BAAa,EAAC,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,EAC7C,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE;oBACxB,0FAA0F;oBAC1F,IAAI,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,CAAA;oBAE1E,IAAI,CAAC,KAAK,GAAG;wBACX,GAAG,OAAO,CAAC,KAAK;wBAChB,EAAE;wBACF,WAAW;qBACZ,CAAA;oBACD,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAA;oBAElB,IAAI,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;oBAE/D,OAAO,MAAM,CAAA;gBACf,CAAC,EACD,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,YAAM,CAAC,KAAK,CAAC,GAAG,OAAO,WAAW,SAAS,GAAG,EAAE,KAAK,CAAC,CAC3E,CAAA;YACH,CAAC,CAAA;YAED,OAAO,WAAW,CAAA;QACpB,CAAC;IACH,CAAC;CACF,CAAC,CAAA;AArCS,QAAA,4BAA4B,gCAqCrC","sourcesContent":["import { defaultFieldResolver, GraphQLSchema } from 'graphql'\nimport gql from 'graphql-tag'\n\nimport { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'\n\nimport { logger } from '@things-factory/env'\n\nimport { getDataSource } from '../../initializers/database.js'\nimport { runTransaction } from '../../utils/after-commit.js'\n\nconst DIRECTIVE = 'transaction'\n\nexport const transactionDirectiveTypeDefs = gql`\n directive @${DIRECTIVE} on FIELD_DEFINITION\n `\nexport const transactionDirectiveResolver = (schema: GraphQLSchema) =>\n mapSchema(schema, {\n [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName) => {\n const transactionDirective = getDirective(schema, fieldConfig, DIRECTIVE)?.[0]\n if (transactionDirective) {\n const { resolve = defaultFieldResolver } = fieldConfig\n\n fieldConfig.resolve = async function (source, args, context, info) {\n /*\n * The resolver gets context.state.afterCommit: steps that cannot be undone wait there and run\n * once the transaction has returned, which is after the commit. On a rollback they are dropped.\n * See utils/after-commit.ts (ruling 2026-09-24, ADR-0091 decision 8).\n */\n return await runTransaction(\n work => getDataSource('tx').transaction(work),\n async (tx, afterCommit) => {\n /* local-graphql-client로부터 invoke인 경우에는 context.req, context.res 가 없으므로, 빈 오브젝트로 대체해준다. */\n let wrap = context.app.createContext(context.req || {}, context.res || {})\n\n wrap.state = {\n ...context.state,\n tx,\n afterCommit\n }\n wrap.t = context.t\n\n let result = await resolve.call(this, source, args, wrap, info)\n\n return result\n },\n (message, error) => logger.error(`${message} (field ${fieldName})`, error)\n )\n }\n\n return fieldConfig\n }\n }\n })\n"]}
|
|
@@ -4,12 +4,12 @@ export * from './domain/domain.js';
|
|
|
4
4
|
export * from './domain/domain-types.js';
|
|
5
5
|
export * from './subscription-data/data-types.js';
|
|
6
6
|
export * from './env-var/env-var.js';
|
|
7
|
-
export declare const entities: (typeof import("./
|
|
7
|
+
export declare const entities: (typeof import("./attribute-set/attribute-set.js").AttributeSet | typeof import("./domain/domain.js").Domain | typeof import("./env-var/env-var.js").EnvVar)[];
|
|
8
8
|
export declare const schema: {
|
|
9
9
|
typeDefs: {
|
|
10
10
|
transactionDirectiveTypeDefs: import("graphql").DocumentNode;
|
|
11
11
|
};
|
|
12
|
-
resolverClasses: (typeof import("./
|
|
12
|
+
resolverClasses: (typeof import("./attribute-set/attribute-set-query.js").AttributeSetQuery | typeof import("./attribute-set/attribute-set-mutation.js").AttributeSetMutation | typeof import("./domain/domain-query.js").DomainQuery | typeof import("./domain/domain-mutation.js").DomainMutation | typeof import("./subscription-data/data-resolver.js").DataResolver | typeof import("./env-var/env-var-query.js").EnvVarQuery | typeof import("./env-var/env-var-mutation.js").EnvVarMutation)[];
|
|
13
13
|
directives: {
|
|
14
14
|
transaction: (schema: import("graphql").GraphQLSchema) => import("graphql").GraphQLSchema;
|
|
15
15
|
};
|