@simplysm/sd-claude 14.0.87 → 14.0.89
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/claude/references/sd-simplysm14/README.md +17 -18
- package/claude/references/sd-simplysm14/apis/angular/README.md +35 -0
- package/claude/references/sd-simplysm14/apis/angular/controls.md +51 -0
- package/claude/references/sd-simplysm14/apis/angular/crud.md +53 -0
- package/claude/references/sd-simplysm14/apis/angular/directives.md +34 -0
- package/claude/references/sd-simplysm14/apis/angular/features.md +40 -0
- package/claude/references/sd-simplysm14/apis/angular/infra.md +74 -0
- package/claude/references/sd-simplysm14/apis/angular/layout.md +27 -0
- package/claude/references/sd-simplysm14/apis/angular/overlay.md +103 -0
- package/claude/references/sd-simplysm14/apis/angular/routing-appstructure.md +69 -0
- package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +28 -0
- package/claude/references/sd-simplysm14/apis/angular/shared-data.md +57 -0
- package/claude/references/sd-simplysm14/apis/angular/sheet.md +73 -0
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +78 -0
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +66 -0
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +71 -0
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +62 -0
- package/claude/references/sd-simplysm14/apis/core-browser/README.md +70 -0
- package/claude/references/sd-simplysm14/apis/core-browser/dom-element.md +62 -0
- package/claude/references/sd-simplysm14/apis/core-browser/indexed-db.md +80 -0
- package/claude/references/sd-simplysm14/apis/core-common/README.md +262 -0
- package/claude/references/sd-simplysm14/apis/core-common/array-ext.md +121 -0
- package/claude/references/sd-simplysm14/apis/core-common/async-runtime.md +128 -0
- package/claude/references/sd-simplysm14/apis/core-common/datetime.md +129 -0
- package/claude/references/sd-simplysm14/apis/core-common/errors.md +91 -0
- package/claude/references/sd-simplysm14/apis/core-common/json-transfer.md +53 -0
- package/claude/references/sd-simplysm14/apis/core-common/obj.md +117 -0
- package/claude/references/sd-simplysm14/apis/core-node/README.md +17 -0
- package/claude/references/sd-simplysm14/apis/core-node/consola.md +43 -0
- package/claude/references/sd-simplysm14/apis/core-node/cpx.md +50 -0
- package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +41 -0
- package/claude/references/sd-simplysm14/apis/core-node/fsx.md +72 -0
- package/claude/references/sd-simplysm14/apis/core-node/pathx.md +39 -0
- package/claude/references/sd-simplysm14/apis/core-node/worker.md +52 -0
- package/claude/references/sd-simplysm14/apis/excel/README.md +43 -0
- package/claude/references/sd-simplysm14/apis/excel/cell.md +54 -0
- package/claude/references/sd-simplysm14/apis/excel/conditional-format.md +51 -0
- package/claude/references/sd-simplysm14/apis/excel/style.md +67 -0
- package/claude/references/sd-simplysm14/apis/excel/utils.md +35 -0
- package/claude/references/sd-simplysm14/apis/excel/workbook-worksheet.md +97 -0
- package/claude/references/sd-simplysm14/apis/excel/wrapper.md +83 -0
- package/claude/references/sd-simplysm14/apis/lint/README.md +49 -0
- package/claude/references/sd-simplysm14/apis/lint/rules.md +130 -0
- package/claude/references/sd-simplysm14/apis/orm-common/README.md +13 -0
- package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +111 -0
- package/claude/references/sd-simplysm14/apis/orm-common/expr.md +128 -0
- package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +145 -0
- package/claude/references/sd-simplysm14/apis/orm-common/schema.md +147 -0
- package/claude/references/sd-simplysm14/apis/orm-common/types.md +62 -0
- package/claude/references/sd-simplysm14/apis/orm-node/README.md +90 -0
- package/claude/references/sd-simplysm14/apis/orm-node/db-conn.md +94 -0
- package/claude/references/sd-simplysm14/apis/sd-cli/README.md +26 -0
- package/claude/references/sd-simplysm14/apis/sd-cli/SdTsCompiler.md +117 -0
- package/claude/references/sd-simplysm14/apis/sd-cli/sd-config-types.md +291 -0
- package/claude/references/sd-simplysm14/apis/service-client/README.md +150 -0
- package/claude/references/sd-simplysm14/apis/service-client/orm.md +48 -0
- package/claude/references/sd-simplysm14/apis/service-client/transport.md +59 -0
- package/claude/references/sd-simplysm14/apis/service-common/README.md +84 -0
- package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +48 -0
- package/claude/references/sd-simplysm14/apis/service-common/protocol.md +72 -0
- package/claude/references/sd-simplysm14/apis/service-server/README.md +118 -0
- package/claude/references/sd-simplysm14/apis/service-server/service-authoring.md +71 -0
- package/claude/references/sd-simplysm14/apis/service-server/transport-internals.md +62 -0
- package/claude/references/sd-simplysm14/apis/service-server/v1-legacy.md +39 -0
- package/claude/references/sd-simplysm14/apis/storage/README.md +120 -0
- package/claude/skills/sd-demo/SKILL.md +6 -0
- package/claude/skills/sd-impl/SKILL.md +4 -7
- package/claude/skills/sd-spec/SKILL.md +31 -858
- package/claude/skills/sd-spec/references/spec-authoring.md +519 -0
- package/claude/workflows/sd-docs.js +84 -0
- package/claude/{skills/sd-docs/references/subagent-prompt.md → workflows/sd-docs.rules.md} +25 -40
- package/package.json +1 -1
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/.specs/inventory/spec.md +0 -99
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/package.json +0 -12
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/index.ts +0 -3
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inbound/inbound.list.ts +0 -150
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/inventory/inventory-master.list.ts +0 -143
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/packages/demo-client/src/screens/outbound/outbound.list.ts +0 -150
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/pnpm-workspace.yaml +0 -2
- package/claude/skills/sd-demo/evals/fixtures/inventory-list/sd.config.ts +0 -12
- package/claude/skills/sd-demo/evals/golden.jsonl +0 -1
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/package.json +0 -8
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/src/.gitkeep +0 -0
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tests/.gitkeep +0 -0
- package/claude/skills/sd-dev/evals/fixtures/minimal-ts-pkg/tsconfig.json +0 -10
- package/claude/skills/sd-dev/evals/golden.jsonl +0 -1
- package/claude/skills/sd-docs/SKILL.md +0 -58
- package/claude/skills/sd-docs/evals/fixtures/new-write/.claude/references/sd-simplysm14/README.md +0 -7
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/bar/src/index.ts +0 -3
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/package.json +0 -6
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/baz/src/index.ts +0 -1
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/new-write/packages/foo/src/index.ts +0 -8
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/README.md +0 -7
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/.claude/references/sd-simplysm14/apis/foo/README.md +0 -3
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/bar/src/index.ts +0 -3
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/package.json +0 -6
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/baz/src/index.ts +0 -1
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/package.json +0 -5
- package/claude/skills/sd-docs/evals/fixtures/update-mixed/packages/foo/src/index.ts +0 -8
- package/claude/skills/sd-docs/evals/golden.jsonl +0 -2
- package/claude/skills/sd-impl/evals/fixtures/case-a-new-screen/.specs/260513120000_warehouse/spec.md +0 -101
- package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/.specs/260513120000_warehouse/spec.md +0 -101
- package/claude/skills/sd-impl/evals/fixtures/case-b-update-with-demo/packages/app/src/screens/box-register/box-register.view.ts +0 -46
- package/claude/skills/sd-impl/evals/fixtures/case-c-new-cross/.specs/260513120000_warehouse/spec.md +0 -89
- package/claude/skills/sd-impl/evals/fixtures/case-d-spec-modify/.specs/260513120000_warehouse/spec.md +0 -101
- package/claude/skills/sd-impl/evals/golden.jsonl +0 -4
- package/claude/skills/sd-manual/evals/fixtures/new-manual/src/notification.ts +0 -25
- package/claude/skills/sd-manual/evals/fixtures/update-manual/.claude/references/sd-simplysm14/manuals/notification.md +0 -14
- package/claude/skills/sd-manual/evals/fixtures/update-manual/src/notification.ts +0 -37
- package/claude/skills/sd-manual/evals/golden.jsonl +0 -2
- package/claude/skills/sd-review/evals/fixtures/code-review/src/foo.ts +0 -7
- package/claude/skills/sd-review/evals/fixtures/doc-review/docs/foo.md +0 -4
- package/claude/skills/sd-review/evals/golden.jsonl +0 -2
- package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +0 -14
- package/claude/skills/sd-skill/evals/fixtures/new-skill/.gitkeep +0 -0
- package/claude/skills/sd-skill/evals/golden.jsonl +0 -2
- package/claude/skills/sd-spec/evals/fixtures/case-a-split//355/232/214/354/235/230/353/241/235.md +0 -20
- package/claude/skills/sd-spec/evals/fixtures/case-b-detail/.specs/260513120000_warehouse/spec.md +0 -95
- package/claude/skills/sd-spec/evals/golden.jsonl +0 -2
- package/claude/skills/sd-unpack/evals/fixtures/eml-with-text-attachment/meeting.eml +0 -21
- package/claude/skills/sd-unpack/evals/fixtures/simple-eml/meeting.eml +0 -10
- package/claude/skills/sd-unpack/evals/golden.jsonl +0 -2
- package/claude/skills/sd-use/evals/fixtures/empty/.gitkeep +0 -0
- package/claude/skills/sd-use/evals/golden.jsonl +0 -6
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @simplysm/lint — rules
|
|
2
|
+
|
|
3
|
+
`eslint-plugin` 이 노출하는 커스텀 ESLint 규칙 9종. 규칙 id 는 등록 시 `@simplysm/<name>`. 개별 규칙을 켜거나 lint 위반 메시지·autofix·옵션 의미를 파악할 때 읽음. 모든 규칙은 `createRule`(= `ESLintUtils.RuleCreator`, `src/utils/create-rule.ts`)로 생성되어 문서 URL 이 자동 부여됨. 옵션 없는 규칙은 `schema: []`. autofix 는 아래 "autofix" 표기 규칙만 제공.
|
|
4
|
+
|
|
5
|
+
## no-hard-private
|
|
6
|
+
|
|
7
|
+
ECMAScript hard private(`#field`) 사용을 금지하고 TypeScript `private _` 스타일을 강제. `type: "problem"`, autofix 있음, 옵션 없음. JS·TS 양쪽에서 동작.
|
|
8
|
+
|
|
9
|
+
검사 대상: 클래스 필드 선언(`#field`), 메서드 선언(`#method()`), 접근자 선언(`accessor #field`), 멤버 접근 표현식(`this.#field`).
|
|
10
|
+
|
|
11
|
+
메시지:
|
|
12
|
+
- `preferSoftPrivate` — hard private(#) 금지, `private _` 스타일 사용 안내. autofix 는 `#name` → `_name` 으로 치환하고, 선언부에 접근제어자가 없으면 `private ` 를 앞에 삽입(데코레이터·`static`·`async`·`readonly` 순서를 보존해 그 뒤 삽입). 토큰 계산 실패 시 이름만 바뀌는 불완전 수정을 막기 위해 수정 전체를 건너뜀.
|
|
13
|
+
- `nameConflict` — `#{{name}}` 을 `_{{name}}` 으로 바꿀 수 없음(동일 이름 멤버가 클래스에 이미 존재). 이 경우 보고만 하고 autofix 안 함. 중첩 클래스는 스택으로 각 클래스 멤버 집합을 관리.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
class A { #count = 0; inc() { this.#count++; } }
|
|
17
|
+
// → private _count = 0; inc() { this._count++; }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## no-subpath-imports-from-simplysm
|
|
21
|
+
|
|
22
|
+
`@simplysm/*` 패키지의 `src` 하위 경로 import 를 금지(빌드 산출물 export 경유를 강제). `type: "problem"`, autofix 있음, 옵션 없음. JS·TS 양쪽에서 동작.
|
|
23
|
+
|
|
24
|
+
검사 대상: 정적 import(`import ... from`), 동적 import(`import("...")`), 재내보내기(`export { x } from`), 전체 재내보내기(`export * from`). import 경로를 `/` 로 분리해 3번째 세그먼트가 `src` 인 경우(`@simplysm/pkg/src`, `@simplysm/pkg/src/xxx`)만 위반. `@simplysm/pkg`·`@simplysm/pkg/xxx`(src 가 아닌 subpath)는 허용.
|
|
25
|
+
|
|
26
|
+
메시지:
|
|
27
|
+
- `noSubpathImport` — `'@simplysm/{{pkg}}'` 에서 `src` 하위 경로 import 불가. autofix 는 경로를 `@simplysm/<pkg>`(원래 따옴표 유지)로 치환.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { x } from "@simplysm/core-common/src/x"; // → "@simplysm/core-common"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## ng-no-async-effect
|
|
34
|
+
|
|
35
|
+
Angular `@angular/core` 의 `effect()` 에 async 함수를 직접 전달하는 것을 금지. `type: "problem"`, autofix 없음, 옵션 없음. `await` 이후 signal read 가 reactive 의존성으로 추적되지 않고 반환값이 `Promise<void>` 가 되어 cleanup 등록이 막히는 문제 방지.
|
|
36
|
+
|
|
37
|
+
검사 대상: `effect(...)` 호출의 첫 인자가 async ArrowFunction/FunctionExpression 인 경우. `effect` 식별자가 `@angular/core` 에서 import 되었는지 스코프로 검증 — named(`import { effect }`)·aliased(`effect as ngEffect`)·namespace(`import * as ng` → `ng.effect(...)`) import 만 인정. 다른 모듈 또는 로컬 선언 `effect` 는 무시.
|
|
38
|
+
|
|
39
|
+
메시지:
|
|
40
|
+
- `noAsyncEffect` — async 함수 직접 전달 금지. 비동기는 `void untracked(async () => { ... })` 내부에서 수행하라고 안내. 보고 위치는 첫 인자(콜백) 노드.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
effect(() => { this.sig(); void untracked(async () => { await this.load(); }); });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## ts-no-throw-not-implemented-error
|
|
47
|
+
|
|
48
|
+
`@simplysm/core-common` 의 `NotImplementedError` 를 `new` 로 인스턴스화하는 코드를 감지(미구현 코드의 프로덕션 유입 방지). `type: "suggestion"`, autofix 없음, 옵션 없음. recommended 에서 `warn`(테스트 파일은 `off`).
|
|
49
|
+
|
|
50
|
+
검사 대상: `new NotImplementedError(...)`(named/aliased import), `new CC.NotImplementedError(...)`(namespace import). 식별자가 `@simplysm/core-common` 에서 import 되었는지 스코프로 검증. 동적 import(`await import(...)`)는 감지 안 함.
|
|
51
|
+
|
|
52
|
+
메시지:
|
|
53
|
+
- `noThrowNotImplementedError` — 메시지 본문은 `{{text}}` 치환. 첫 인자가 비어있지 않은 문자열 리터럴이면 그 값을, 아니면 `"미구현"` 을 출력.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { NotImplementedError } from "@simplysm/core-common";
|
|
57
|
+
new NotImplementedError("결제 연동"); // → "결제 연동" 경고
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## ts-no-unused-injects
|
|
61
|
+
|
|
62
|
+
미사용 Angular `inject()` 필드를 감지. `type: "problem"`, autofix 있음, 옵션 없음.
|
|
63
|
+
|
|
64
|
+
검사 대상: 클래스 본문에서 `inject(...)` 호출로 초기화된 PropertyDefinition(키가 Identifier). 같은 클래스 본문 전체를 순회해 필드명과 동일한 Identifier 참조(필드 키 자신 제외)가 0개면 미사용으로 판정. 클래스 내부 참조만 검사 — 템플릿 사용 여부는 보지 않음.
|
|
65
|
+
|
|
66
|
+
메시지:
|
|
67
|
+
- `unusedInject` — `inject() field "{{name}}" is never used.` autofix 는 해당 필드 선언을 앞 토큰 끝부터 제거(뒤 토큰이 있으면 필드 끝까지).
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
class C { private _svc = inject(MyService); } // _svc 미참조 시 제거
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## ts-no-unused-protected-readonly
|
|
74
|
+
|
|
75
|
+
Angular `@Component` 의 인라인 템플릿·클래스 본문 어디에서도 안 쓰이는 `protected readonly` 필드를 감지. `type: "problem"`, autofix 있음, 옵션 없음.
|
|
76
|
+
|
|
77
|
+
검사 대상: `@Component` 데코레이터가 있고 그 첫 인자 객체에 `template` 속성(문자열 리터럴 또는 템플릿 리터럴)이 있는 클래스. 그 클래스의 `protected readonly` 비-static 필드(키가 Identifier)가 ① 인라인 템플릿에서 미참조 ② 클래스 본문 다른 멤버에서 미참조 둘 다일 때 보고. 템플릿 식별자는 `@angular/compiler` 의 `parseTemplate` 으로 AST 파싱 후 `ImplicitReceiver`/`ThisReceiver` 위 `PropertyRead`(클래스 필드 참조)만 수집하며, `*ngFor` 로컬·`@let`·`@if ... as`·`@for` item/별칭 등 스코프 로컬 변수는 제외. `@if`/`@switch`/`@for`/`@defer` 블록, 입력/이벤트/구조 디렉티브 바인딩까지 순회. `templateUrl`(외부 템플릿)은 대상 아님(`template` 문자열만).
|
|
78
|
+
|
|
79
|
+
메시지:
|
|
80
|
+
- `unusedField` — `Protected readonly field "{{name}}" is not used in class or template.` autofix 는 필드 선언과 앞 들여쓰기·뒤 `;`·개행을 함께 제거.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
@Component({ template: `<div>{{title}}</div>` })
|
|
84
|
+
class C { protected readonly title = "x"; protected readonly unused = 1; } // unused 제거
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## ng-template-no-strict-null-check
|
|
88
|
+
|
|
89
|
+
Angular HTML 템플릿에서 엄격 비교(`=== null`, `!== null`, `=== undefined`, `!== undefined`)를 금지하고 `== null`/`!= null` 로 통일하도록 강제. `type: "problem"`, autofix 없음(인라인 템플릿 offset 매핑 문제로 미제공), 옵션 없음. HTML 파일 대상.
|
|
90
|
+
|
|
91
|
+
검사 대상: 템플릿 표현식의 `Binary` 노드 중 연산자가 `===`/`!==` 이고 양변 중 하나가 nil 리터럴(value 가 null/undefined)인 경우.
|
|
92
|
+
|
|
93
|
+
메시지:
|
|
94
|
+
- `noStrictNullCheck` — `{{actual}}`(예: `x === null`) 사용 금지, `{{replacement}}`(예: `x == null`) 사용 안내. `===`→`==`, `!==`→`!=` 로 변환한 권장 표현을 제시.
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
@if (user !== null) {} <!-- user != null 사용하라고 보고 -->
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## ng-template-no-todo-comments
|
|
101
|
+
|
|
102
|
+
Angular HTML 템플릿 내 `<!-- TODO: ... -->` 주석을 경고. `type: "problem"`, autofix 없음, 옵션 없음. recommended 에서 `warn`. HTML 파일 대상.
|
|
103
|
+
|
|
104
|
+
동작: raw 텍스트를 `<!--...-->` 정규식으로 훑어 주석 내용에 `TODO:` 가 있으면 보고(AST 방문자 없이 빈 객체 반환). 메시지 본문은 `TODO:` 뒤 trim 한 내용.
|
|
105
|
+
|
|
106
|
+
메시지:
|
|
107
|
+
- `noTodo` — 본문은 `{{content}}`(TODO 뒤 텍스트) 그대로 출력.
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<!-- TODO: 페이징 추가 --> <!-- "페이징 추가" 경고 -->
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## ng-template-sd-require-binding-attrs
|
|
114
|
+
|
|
115
|
+
`sd-*` 접두사 커스텀 컴포넌트에서 허용목록 밖 plain attribute 사용을 금지하고 Angular property binding(`[attr]="..."`)을 강제. `type: "problem"`, autofix 있음. HTML 파일 대상. **유일하게 옵션 있는 규칙**.
|
|
116
|
+
|
|
117
|
+
옵션(`RuleOptions`, 모두 선택):
|
|
118
|
+
- `selectorPrefixes: string[]` — 검사 대상 요소 태그 접두사. 미지정 시 `["sd-"]`. 태그명을 소문자로 비교. 다른 디자인 시스템 접두사를 검사하려면 지정.
|
|
119
|
+
- `allowAttributes: string[]` — plain attribute 로 허용할 정확한 이름 목록. 미지정 시 `["id","class","style","title","tabindex","role"]`. 소문자 비교. 추가로 허용할 표준 속성을 늘릴 때.
|
|
120
|
+
- `allowAttributePrefixes: string[]` — plain attribute 로 허용할 접두사 목록. 미지정 시 `["aria-","data-","sd-"]`. 소문자 비교. 접두사 기반(aria/data 등) 속성군을 통째 허용할 때.
|
|
121
|
+
|
|
122
|
+
동작: 대상 태그(접두사 매칭) 요소의 attribute 중 `allowAttributes`(정확 일치)·`allowAttributePrefixes`(접두사 일치) 어디에도 안 드는 것을 보고.
|
|
123
|
+
|
|
124
|
+
메시지:
|
|
125
|
+
- `requireBindingForAttribute` — `"{{attrName}}"` 은 `"{{elementName}}"` 의 plain attribute 로 불가, property binding 사용 안내. autofix 는 값이 빈 문자열이면 `[attr]="true"`, 값이 있으면 `\` 와 `'` 를 이스케이프해 `[attr]="'값'"` 로 치환(span 의 start≥end 면 수정 안 함).
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<sd-button myattr="hello"></sd-button> <!-- → <sd-button [myattr]="'hello'"></sd-button> -->
|
|
129
|
+
<sd-button disabled></sd-button> <!-- → <sd-button [disabled]="true"></sd-button> -->
|
|
130
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @simplysm/orm-common
|
|
2
|
+
|
|
3
|
+
Dialect 독립 ORM 코어. 테이블/뷰/프로시저를 fluent builder 로 정의하고, `DbContext` 클래스에 등록한 뒤, 체이닝 `Queryable` 로 타입 안전한 SELECT/CUD 쿼리를 JSON AST(`QueryDef`/`Expr`)로 조립한다. 실제 SQL 변환은 이 패키지의 dialect QueryBuilder(MySQL/MSSQL/PostgreSQL)가, DB 연결·실행은 `DbContextExecutor` 구현체(`@simplysm/orm-node` 등)가 담당한다.
|
|
4
|
+
|
|
5
|
+
## 사용 트리거 인덱스
|
|
6
|
+
|
|
7
|
+
- **스키마 정의 (Table/View/Procedure/Column/Index/Relation 빌더)** — DB 객체를 fluent 빌더로 선언하고 column·PK·index·FK 관계를 잡을 때. 자세히: [schema.md](./schema.md)
|
|
8
|
+
- **DbContext (연결·트랜잭션·DDL·마이그레이션·초기화·트랜잭션 에러)** — 빌더들을 한 컨텍스트에 등록하고 `connect`/`transaction` 으로 실행, DDL·migration·`initialize` 를 돌리거나 `DbTransactionError` 를 처리할 때. 자세히: [db-context.md](./db-context.md)
|
|
9
|
+
- **Queryable (SELECT/JOIN/CUD 체이닝 · 프로시저 실행 · 검색 파서)** — `db.user()` 로 받은 쿼리 빌더에 where/orderBy/join/include/group/recursive/union 을 걸고 execute/single/count/insert/update/delete/upsert 하거나, `Executable` 로 프로시저를 실행하고 `search()` 텍스트 검색을 쓸 때. 자세히: [queryable.md](./queryable.md)
|
|
10
|
+
- **expr (SQL 표현식 빌더)** — where/select/orderBy 콜백 안에서 비교·문자열·숫자·날짜·집계·조건·window·서브쿼리 표현식을 만들 때. 자세히: [expr.md](./expr.md)
|
|
11
|
+
- **타입·실행 엔진 내부 (QueryDef/Expr/Column 타입 · dialect QueryBuilder · 결과 파서)** — executor·QueryBuilder 를 직접 구현하거나 AST·결과 메타를 다루고, QueryDef 를 SQL 로 렌더링하거나 raw 결과를 환원할 때. 자세히: [types.md](./types.md)
|
|
12
|
+
|
|
13
|
+
모든 군이 별도 파일로 분할되어 있다. README 인라인 섹션은 두지 않는다.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @simplysm/orm-common — DbContext / 연결·트랜잭션·DDL·마이그레이션
|
|
2
|
+
|
|
3
|
+
`DbContext` 추상 클래스를 상속해 테이블·뷰·프로시저를 프로퍼티로 등록하고, 연결·트랜잭션·DDL·마이그레이션을 실행하는 묶음. executor(`DbContextExecutor`) 구현체와 `{ database, schema? }` 옵션을 생성자로 주입한다. 트랜잭션 롤백 에러는 `DbTransactionError` 로 표준화된다.
|
|
4
|
+
|
|
5
|
+
## DbContext (정의·등록)
|
|
6
|
+
|
|
7
|
+
`abstract class DbContext implements DbContextBase`. 서브클래스에서 `this.queryable(...)`/`this.executable(...)` 로 멤버를 만들고 `migrations` 를 오버라이드한다.
|
|
8
|
+
|
|
9
|
+
- `constructor(executor: DbContextExecutor, opt: { database: string; schema?: string })` — executor 와 대상 DB 옵션 주입.
|
|
10
|
+
- `protected queryable(builder): () => Queryable` — Table/View 빌더를 Queryable 팩토리로 등록. 호출 시마다 새 alias 부여(View 는 viewFn 실행). 반환 함수에 `SD_BUILDER` 심볼로 원본 빌더를 부착.
|
|
11
|
+
- `protected executable(builder): () => Executable` — Procedure 빌더를 Executable 팩토리로 등록. 마찬가지로 `SD_BUILDER` 부착.
|
|
12
|
+
- `migrations: Migration[]` — 마이그레이션 정의 배열. 서브클래스에서 오버라이드. `initialize()` 가 미적용분만 순서대로 실행.
|
|
13
|
+
- `_migration` — 내부 시스템 마이그레이션 테이블(`_Migration`) Queryable. 적용 이력 저장용.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
class MainDb extends DbContext {
|
|
17
|
+
user = this.queryable(User);
|
|
18
|
+
post = this.queryable(Post);
|
|
19
|
+
getUserById = this.executable(GetUserById);
|
|
20
|
+
override migrations = [{ name: "001", up: async (db) => { await db.createTable(User); } }];
|
|
21
|
+
}
|
|
22
|
+
const db = new MainDb(executor, { database: "mydb" });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 상태·내부 (DbContextBase / DbContextStatus)
|
|
26
|
+
|
|
27
|
+
`interface DbContextBase` — Queryable/Executable/View 가 의존하는 핵심 면. DbContext 가 구현. executor·어댑터 작성 시 참조.
|
|
28
|
+
|
|
29
|
+
- `status: DbContextStatus` — `"ready"`(미연결) / `"connect"`(연결됨, 트랜잭션 밖) / `"transact"`(트랜잭션 중). `transact` 상태에서 DDL 실행 시 throw.
|
|
30
|
+
- `readonly database` / `readonly schema` — 주입된 옵션 값(`string | undefined`).
|
|
31
|
+
- `getNextAlias(): string` — 다음 테이블 alias(`T1`, `T2`...) 발급. 카운터 증가.
|
|
32
|
+
- `resetAliasCounter(): void` — alias 카운터 0 으로. connect 시작 시 호출됨.
|
|
33
|
+
- `executeDefs<T>(defs, resultMetas?): Promise<T[][]>` — QueryDef 배열 실행 위임. `transact` 중 DDL 포함 시 throw. 모든 쿼리 실행의 단일 통로.
|
|
34
|
+
- `getQueryDefObjectName(tableOrView): QueryDefObjectName` — 빌더에서 `{database, schema, name}` 추출.
|
|
35
|
+
- `switchFk(table, enabled): Promise<void>` — FK 제약 on/off(트랜잭션 내 허용).
|
|
36
|
+
|
|
37
|
+
`type DbContextStatus = "ready" | "connect" | "transact"`. `interface DbContextDdlMethods` 는 아래 DDL 메서드들의 시그니처 모음(`Migration.up` 의 인자 타입).
|
|
38
|
+
|
|
39
|
+
## 연결·트랜잭션
|
|
40
|
+
|
|
41
|
+
- `connect<T>(fn, isolationLevel?): Promise<T>` — 연결+트랜잭션으로 `fn` 실행. 최초 1회 관계 검증(`validateRelations`) 후 alias 리셋, beginTransaction → fn → commit, 예외 시 rollback 후 rethrow, 끝에 항상 close. `ready` 상태 아니면 throw. 일반적인 "한 작업=한 트랜잭션" 경로.
|
|
42
|
+
- `connectWithoutTransaction<T>(callback): Promise<T>` — 연결만(트랜잭션 없이) `callback` 실행 후 close. DDL·여러 독립 트랜잭션을 그 안에서 직접 열 때.
|
|
43
|
+
- `transaction<T>(fn, isolationLevel?): Promise<T>` — 이미 연결된 상태에서 트랜잭션 1개 실행(begin→fn→commit, 실패 시 rollback). `transact` 중 재호출하면 throw. `connectWithoutTransaction` 안에서 트랜잭션을 나눠 쓸 때.
|
|
44
|
+
- `isolationLevel` — `"READ_UNCOMMITTED"|"READ_COMMITTED"|"REPEATABLE_READ"|"SERIALIZABLE"`. 미지정 시 DB 기본값. 동시성 요구에 따라 선택.
|
|
45
|
+
|
|
46
|
+
롤백 중 발생한 에러가 `DbTransactionError` 이고 코드가 `NO_ACTIVE_TRANSACTION` 이면 무시(원 에러 보존), 그 외엔 원 에러의 `cause` 로 부착.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
await db.connect(async () => {
|
|
50
|
+
const user = await db.user().where((u) => [expr.eq(u.id, 1)]).lock().single();
|
|
51
|
+
await db.user().where((u) => [expr.eq(u.id, 1)]).update((u) => ({ name: expr.val("string", "X") }));
|
|
52
|
+
}, "REPEATABLE_READ");
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## DDL 실행 메서드
|
|
56
|
+
|
|
57
|
+
각 메서드는 해당 QueryDef 1개를 `executeDefs` 로 실행한다(`transact` 중 호출 시 throw). 대상 `table`/`view`/`procedure` 인자는 빌더 또는 `QueryDefObjectName`.
|
|
58
|
+
|
|
59
|
+
- `createTable(table)` / `dropTable(table)` / `renameTable(table, newName)` — 테이블 생성·삭제·이름변경.
|
|
60
|
+
- `createView(view)` / `dropView(view)` — 뷰 생성·삭제.
|
|
61
|
+
- `createProc(procedure)` / `dropProc(procedure)` — 프로시저 생성·삭제.
|
|
62
|
+
- `addColumn(table, columnName, column)` / `dropColumn(table, column)` / `modifyColumn(table, columnName, column)` / `renameColumn(table, column, newName)` — 컬럼 추가·삭제·변경·이름변경. `column` 은 `ColumnBuilder`.
|
|
63
|
+
- `addPrimaryKey(table, columns)` / `dropPrimaryKey(table)` — PK 추가·삭제.
|
|
64
|
+
- `addForeignKey(table, relationName, relationDef)` / `dropForeignKey(table, relationName)` — FK 추가·삭제. `relationDef` 는 `ForeignKeyBuilder`.
|
|
65
|
+
- `addIndex(table, indexBuilder)` / `dropIndex(table, columns)` — 인덱스 추가·삭제.
|
|
66
|
+
- `truncate(table)` — 데이터 전체 삭제(DDL 취급).
|
|
67
|
+
- `clearSchema(params: { database, schema? })` — 스키마 내 모든 객체 삭제. 초기화·테스트 정리에.
|
|
68
|
+
- `schemaExists(database, schema?): Promise<boolean>` — 스키마 존재 여부.
|
|
69
|
+
- `switchFk(table, enabled): Promise<void>` — FK 제약 일시 on/off(트랜잭션 내 허용, DDL 아님).
|
|
70
|
+
|
|
71
|
+
`get...QueryDef(...)` 형태의 생성기(`getCreateTableQueryDef` 등 위 메서드 1:1 대응 + `getCreateObjectQueryDef`)는 실행 없이 `QueryDef` 만 반환 — 여러 DDL 을 모아 한 번에 실행하거나 SQL 을 미리 확인할 때.
|
|
72
|
+
|
|
73
|
+
## 초기화 / Migration
|
|
74
|
+
|
|
75
|
+
- `initialize(options?: { dbs?: string[]; force?: boolean }): Promise<boolean>` — 스키마·마이그레이션 적용. `dbs`=대상 DB 한정, `force`=강제 재생성. 적용 여부 반환.
|
|
76
|
+
- `interface Migration` — `{ name: string; up: (db: DbContextBase & DbContextDdlMethods) => Promise<void> }`. `name` 은 고유(타임스탬프 권장), `up` 안에서 DDL 호출. `initialize` 가 미적용 `name` 만 순서대로 실행.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
override migrations: Migration[] = [
|
|
80
|
+
{ name: "20260105_001_create_user", up: async (db) => { await db.createTable(User); } },
|
|
81
|
+
{ name: "20260105_002_add_email", up: async (db) => { await db.addColumn(User, "email", c.varchar(200).nullable()); } },
|
|
82
|
+
];
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 트랜잭션 에러 (DbTransactionError / DbErrorCode)
|
|
86
|
+
|
|
87
|
+
DBMS 네이티브 트랜잭션 에러를 표준 코드로 래핑. `connect`/`transaction` 의 롤백 경로에서 던져진다(주로 executor 구현이 생성).
|
|
88
|
+
|
|
89
|
+
- `class DbTransactionError extends Error`
|
|
90
|
+
- `code: DbErrorCode` — 표준 에러 코드(생성자 1번째 인자).
|
|
91
|
+
- `message: string` — 메시지(생성자 2번째 인자).
|
|
92
|
+
- `originalError?: unknown` — 원본 DBMS 에러(생성자 3번째, 디버깅용).
|
|
93
|
+
- `name` — 항상 `"DbTransactionError"`.
|
|
94
|
+
- `enum DbErrorCode` (문자열 값)
|
|
95
|
+
- `NO_ACTIVE_TRANSACTION` — 활성 트랜잭션 없는데 ROLLBACK 시도. 롤백 중 이 코드면 컨텍스트가 무시하고 원 에러를 보존.
|
|
96
|
+
- `TRANSACTION_ALREADY_STARTED` — 이미 트랜잭션 시작됨.
|
|
97
|
+
- `DEADLOCK` — 데드락.
|
|
98
|
+
- `LOCK_TIMEOUT` — 잠금 타임아웃.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
try {
|
|
102
|
+
await db.rollbackTransaction();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof DbTransactionError && err.code === DbErrorCode.NO_ACTIVE_TRANSACTION) return;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## SD_BUILDER
|
|
110
|
+
|
|
111
|
+
- `const SD_BUILDER: unique symbol` — `queryable()`/`executable()` 가 반환한 팩토리 함수에 원본 빌더(TableBuilder/ViewBuilder/ProcedureBuilder)를 부착하는 심볼 키. 컨텍스트 멤버에서 정의 빌더를 역으로 꺼낼 때(예: 전체 테이블 순회·DDL 자동화) 사용.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @simplysm/orm-common — expr (SQL 표현식 빌더)
|
|
2
|
+
|
|
3
|
+
`expr` 객체로 dialect 독립 SQL 표현식을 JSON AST(`Expr`)로 조립한다. where/having 콜백은 `WhereExprUnit[]` 를, select/orderBy/groupBy/update 콜백은 `ExprUnit<T>` 또는 리터럴을 반환한다. 모든 인자는 `ExprInput<T>`(= `ExprUnit<T> | T`) — 컬럼·중첩 식·리터럴을 섞어 넘길 수 있다.
|
|
4
|
+
|
|
5
|
+
핵심 래퍼:
|
|
6
|
+
|
|
7
|
+
- `class ExprUnit<TPrimitive>` — 타입 추적 표현식 래퍼. `dataType: ColumnPrimitiveStr`, `expr: Expr`(AST), `get n` (= 반환 타입에서 `undefined` 제거한 non-null 버전, nullable 컬럼을 비-nullable 로 단언할 때).
|
|
8
|
+
- `class WhereExprUnit` — WHERE 조건 래퍼(`expr: WhereExpr`). 비교/논리 연산이 반환.
|
|
9
|
+
- `type ExprInput<T> = ExprUnit<T> | T` — 표현식 또는 리터럴 입력.
|
|
10
|
+
- `interface SwitchExprBuilder<T>` — `switch()` 가 반환하는 CASE 빌더(`.case(cond, then)` 체이닝 + `.default(value)` 종료).
|
|
11
|
+
- `expr.toExpr(value): Expr` — `ExprInput` 을 `Expr` AST 로 변환(내부용, 직접 AST 다룰 때).
|
|
12
|
+
|
|
13
|
+
## 값 생성 (val / col / raw)
|
|
14
|
+
|
|
15
|
+
- `val(dataType, value): ExprUnit` — 리터럴을 ExprUnit 으로. `dataType`=`"string"|"number"|"boolean"|"DateTime"|"DateOnly"|"Time"|"Uuid"|"Bytes"`. `value`=값(undefined 허용 시 nullable 반환). update/insert 값에 타입을 명시할 때.
|
|
16
|
+
- `col(dataType, ...path): ExprUnit` — 컬럼 참조 직접 생성(보통 콜백 프록시가 대신). `path`=alias·컬럼명 분절.
|
|
17
|
+
- `raw(dataType)\`SQL ${val}\`: ExprUnit` — 이스케이프 해치. ORM 미지원 DB 함수를 태그드 템플릿으로. 보간 값은 자동 파라미터화. `dataType`=반환 타입.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
expr.val("string", "active")
|
|
21
|
+
expr.raw("string")`JSON_EXTRACT(${u.metadata}, '$.email')`
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## WHERE — 비교 (eq / gt / lt / gte / lte / between)
|
|
25
|
+
|
|
26
|
+
전부 `(source: ExprUnit<T>, target: ExprInput<T>) => WhereExprUnit`(between 제외).
|
|
27
|
+
|
|
28
|
+
- `eq(source, target)` — `=` (NULL 안전: MySQL `<=>`, MSSQL/PG `IS NULL OR =`).
|
|
29
|
+
- `gt` / `lt` / `gte` / `lte` — `>` / `<` / `>=` / `<=`.
|
|
30
|
+
- `between(source, from?, to?)` — BETWEEN. `from`/`to` undefined 면 그 방향 무제한(`from`만 주면 `>=`, `to`만 주면 `<=`).
|
|
31
|
+
|
|
32
|
+
## WHERE — NULL / 문자열 / IN / EXISTS
|
|
33
|
+
|
|
34
|
+
- `null(source): WhereExprUnit` — `IS NULL`.
|
|
35
|
+
- `like(source, pattern)` — `LIKE`(`%`=다수, `_`=단일, `\` 이스케이프). 부분/접두 검색.
|
|
36
|
+
- `regexp(source, pattern)` — 정규식 매칭(구문은 DBMS별 상이).
|
|
37
|
+
- `in(source, values: ExprInput[])` — `IN (값목록)`.
|
|
38
|
+
- `inQuery(source, query: Queryable)` — `IN (SELECT ...)`. 서브쿼리가 단일 컬럼만 SELECT 해야 함(아니면 throw).
|
|
39
|
+
- `exists(query: Queryable)` — `EXISTS (SELECT ...)`. SELECT 절은 제거해 패킷 절약. 상관 서브쿼리로 존재 검사.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
db.user().where((u) => [expr.in(u.status, ["active", "pending"]), expr.exists(db.order().where((o) => [expr.eq(o.userId, u.id)]))])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## WHERE — 논리 (not / and / or)
|
|
46
|
+
|
|
47
|
+
- `not(arg: WhereExprUnit)` — 조건 부정.
|
|
48
|
+
- `and(conditions: WhereExprUnit[])` — AND 결합. 빈 배열이면 throw.
|
|
49
|
+
- `or(conditions: WhereExprUnit[])` — OR 결합. 빈 배열이면 throw.
|
|
50
|
+
|
|
51
|
+
## SELECT — 문자열
|
|
52
|
+
|
|
53
|
+
- `concat(...args)` — CONCAT(NULL→빈문자). 반환 `ExprUnit<string>`.
|
|
54
|
+
- `left(source, length)` / `right(source, length)` — 좌/우에서 length 글자.
|
|
55
|
+
- `trim(source)` — 양쪽 공백 제거.
|
|
56
|
+
- `padStart(source, length, fillString)` — LPAD, 대상 길이까지 왼쪽 채움.
|
|
57
|
+
- `replace(source, from, to)` — 문자열 치환.
|
|
58
|
+
- `upper(source)` / `lower(source)` — 대/소문자.
|
|
59
|
+
- `length(source)` — 문자 수(반환 `number`). `byteLength(source)` — 바이트 수(UTF-8 CJK 3B).
|
|
60
|
+
- `substring(source, start, length?)` — 부분 추출(1-기반 인덱스, length 생략 시 끝까지).
|
|
61
|
+
- `indexOf(source, search)` — 위치(1-기반, 없으면 0).
|
|
62
|
+
|
|
63
|
+
(string 입력류는 `<T extends string|undefined>` 로 nullable 전파.)
|
|
64
|
+
|
|
65
|
+
## SELECT — 숫자
|
|
66
|
+
|
|
67
|
+
- `abs(source)` — 절대값. `round(source, digits)` — 반올림(digits 자리). `ceil(source)` — 올림. `floor(source)` — 내림. 모두 nullable 전파.
|
|
68
|
+
|
|
69
|
+
## SELECT — 날짜
|
|
70
|
+
|
|
71
|
+
- `year/month/day(source)` — DateTime|DateOnly 에서 연/월/일(`number`). `hour/minute/second(source)` — DateTime|Time 에서 시/분/초.
|
|
72
|
+
- `isoWeek(source)` — ISO 주번호(1~53). `isoWeekStartDate(source)` — 주의 월요일(`DateOnly`). `isoYearMonth(source)` — 해당 월 1일(`DateOnly`).
|
|
73
|
+
- `dateDiff(unit, from, to)` — 날짜 차(`to - from`). `unit`=`DateUnit`(`"year"|"month"|"day"|"hour"|"minute"|"second"`).
|
|
74
|
+
- `dateAdd(unit, source, value)` — 날짜 더하기(음수 허용, 결과 타입은 source 와 동일).
|
|
75
|
+
- `formatDate(source, format)` — 포맷 문자열(`"%Y-%m-%d"`, 규칙 DBMS별 상이) → `string`.
|
|
76
|
+
|
|
77
|
+
(nullable source 면 결과도 nullable.)
|
|
78
|
+
|
|
79
|
+
## SELECT — 조건 (coalesce / nullIf / is / switch / if)
|
|
80
|
+
|
|
81
|
+
- `coalesce(...args)` — 첫 non-null(COALESCE). 마지막 인자가 non-nullable 이면 결과도 non-nullable.
|
|
82
|
+
- `nullIf(source, value)` — `source === value` 면 NULL, 아니면 source(빈문자→NULL 정규화 등). 결과 nullable.
|
|
83
|
+
- `is(condition: WhereExprUnit): ExprUnit<boolean>` — WHERE 조건을 boolean 컬럼으로(SELECT 절에서).
|
|
84
|
+
- `switch<T>(): SwitchExprBuilder<T>` — CASE WHEN. `.case(cond, then)` 체이닝 후 `.default(value)` 로 종료. then/default 중 하나는 non-null 이어야 타입 추론(전부 null 이면 throw).
|
|
85
|
+
- `if<T>(condition, then, else_): ExprUnit<T>` — 삼항(IF). then/else 중 하나는 non-null 필요(아니면 throw).
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
db.user().select((u) => ({
|
|
89
|
+
grade: expr.switch<string>().case(expr.gte(u.score, 90), "A").case(expr.gte(u.score, 80), "B").default("F"),
|
|
90
|
+
isAdult: expr.is(expr.gte(u.age, 18)),
|
|
91
|
+
}))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## SELECT — 집계 (count / sum / avg / max / min)
|
|
95
|
+
|
|
96
|
+
집계는 행이 없거나 전부 NULL 일 때만 NULL(NULL 행은 무시). count 만 항상 `number`, 나머지는 nullable.
|
|
97
|
+
|
|
98
|
+
- `count(arg?, distinct?)` — 행 수. `arg` 생략 시 전체, `distinct:true` 면 중복 제거.
|
|
99
|
+
- `sum(arg)` / `avg(arg)` — number 컬럼 합/평균(`number|undefined`).
|
|
100
|
+
- `max(arg)` / `min(arg)` — 임의 타입 최대/최소(`T|undefined`, 타입은 arg 따라감).
|
|
101
|
+
|
|
102
|
+
## SELECT — 기타 (greatest / least / rowNum / random / cast / subquery)
|
|
103
|
+
|
|
104
|
+
- `greatest(...args)` / `least(...args)` — 여러 값 중 최대/최소(인자 중 ExprUnit 1개 이상 필요, 없으면 throw).
|
|
105
|
+
- `rowNum(): ExprUnit<number>` — 행 순번(1-기반). `random(): ExprUnit<number>` — 0~1 난수(무작위 정렬용).
|
|
106
|
+
- `cast(source, targetType: DataType): ExprUnit` — 타입 변환. `targetType`=`{ type: "varchar", length: 20 }` 등. 결과 타입은 targetType 으로 추론, nullable 전파.
|
|
107
|
+
- `subquery(dataType, queryable): ExprUnit` — 스칼라 서브쿼리(1행 1컬럼). SELECT 절에서 상관 집계 등에. 결과 nullable.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
db.user().select((u) => ({ id: u.id, postCount: expr.subquery("number", db.post().where((p) => [expr.eq(p.userId, u.id)]).select(() => ({ c: expr.count() }))) }))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## SELECT — Window 함수
|
|
114
|
+
|
|
115
|
+
전부 `spec: { partitionBy?: ExprInput[]; orderBy?: [ExprInput, ("ASC"|"DESC")?][] }` 를 받아 OVER 절 구성.
|
|
116
|
+
|
|
117
|
+
- `rowNumber(spec): ExprUnit<number>` — ROW_NUMBER(파티션 내 1-기반 순번).
|
|
118
|
+
- `rank(spec)` / `denseRank(spec)` — RANK(동순위 후 건너뜀: 1,1,3) / DENSE_RANK(연속: 1,1,2).
|
|
119
|
+
- `ntile(n, spec)` — NTILE, 파티션을 n 그룹으로(1~n).
|
|
120
|
+
- `lag(column, spec, options?)` / `lead(column, spec, options?)` — 이전/다음 행 값. `options.offset`(기본 1), `options.default`(없을 때 기본값). 결과 nullable.
|
|
121
|
+
- `firstValue(column, spec)` / `lastValue(column, spec)` — 프레임 첫/마지막 값(nullable).
|
|
122
|
+
- `sumOver(column, spec)` / `avgOver(column, spec)` — 윈도우 합/평균(누적합·이동평균).
|
|
123
|
+
- `countOver(spec, column?)` — 윈도우 행 수(`column` 생략 시 전체).
|
|
124
|
+
- `minOver(column, spec)` / `maxOver(column, spec)` — 윈도우 최소/최대(nullable).
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
db.order().select((o) => ({ ...o, runningTotal: expr.sumOver(o.amount, { partitionBy: [o.userId], orderBy: [[o.createdAt, "ASC"]] }) }))
|
|
128
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @simplysm/orm-common — Queryable (쿼리 작성·실행 · 프로시저 · 검색)
|
|
2
|
+
|
|
3
|
+
`db.user()` 처럼 컨텍스트 멤버를 호출하면 `Queryable<TData, TFrom>` 를 받는다. immutable 체이닝으로 옵션·필터·조인·그룹을 쌓고, 종단 메서드(execute/single/count/insert/...)로 실행한다. where/select/orderBy 콜백 안에서는 [expr.md](./expr.md) 의 `expr` 로 표현식을 만든다. 프로시저는 `Executable`, 텍스트 검색 구문은 `parseSearchQuery` 가 처리한다.
|
|
4
|
+
|
|
5
|
+
`Queryable<TData, TFrom>`: `TData`=결과 행 타입(컬럼+조인). `TFrom`=소스 TableBuilder(CUD 가능 여부). select/groupBy/join 등으로 컬럼 구조가 바뀌면 `TFrom` 이 `never` 가 되어 CUD 불가.
|
|
6
|
+
|
|
7
|
+
## 옵션 (select / distinct / lock)
|
|
8
|
+
|
|
9
|
+
- `select(fn: (cols) => R): Queryable<...>` — 출력 컬럼 재정의. `fn` 은 원본 컬럼(`QueryableRecord`)을 받아 새 구조(ExprUnit·리터럴·중첩 객체) 반환. raw 리터럴은 자동 ExprUnit 래핑. 이후 CUD 불가.
|
|
10
|
+
- `distinct(): Queryable` — DISTINCT 적용. count() 전이면 `wrap()` 필요.
|
|
11
|
+
- `lock(): Queryable` — FOR UPDATE 행 잠금. 트랜잭션 내 배타 잠금 획득용. `TFrom` 유지.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
db.user().select((u) => ({ userName: u.name, upper: expr.upper(u.email) }))
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 행 제한 (top / limit)
|
|
18
|
+
|
|
19
|
+
- `top(count: number): Queryable` — 상위 N 행(ORDER BY 없이도 가능). first()/exists() 가 내부적으로 `top(1)` 사용.
|
|
20
|
+
- `limit(skip, take): Queryable` — OFFSET/LIMIT 페이지네이션. `skip`=건너뛸 수, `take`=가져올 수. **ORDER BY 선행 필수**, 없으면 throw.
|
|
21
|
+
|
|
22
|
+
## 정렬 (orderBy)
|
|
23
|
+
|
|
24
|
+
- `orderBy(fnOrKey, orderBy?): Queryable` — 정렬 조건 추가(여러 번 호출 시 순서 누적). `fnOrKey`=컬럼 반환 함수 또는 체인 경로 문자열(`"user.name"`, 동적 정렬용 `obj.getChainValue`). `orderBy`=`"ASC"|"DESC"`, 기본 ASC.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
db.user().orderBy((u) => u.name).orderBy((u) => u.age, "DESC").orderBy("id", "DESC")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 필터 (where / search)
|
|
31
|
+
|
|
32
|
+
- `where(predicate: (cols) => WhereExprUnit[]): Queryable` — WHERE 조건 추가. 배열 내 여러 조건·여러 번 호출 모두 AND 결합.
|
|
33
|
+
- `search(fn: (cols) => ExprUnit<string|undefined>[], searchText): Queryable` — 텍스트 검색. `fn`=검색 대상 컬럼들. `searchText` 를 `parseSearchQuery` 로 파싱해 컬럼별 `LIKE lower(...)` 조건 생성(OR 묶음 + 필수 AND + 제외 NOT). `searchText` 가 공백이면 self 반환(조건 무추가).
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
db.user().where((u) => [expr.eq(u.isActive, true)]).search((u) => [u.name, u.email], "John -withdrawn")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 그룹 (groupBy / having)
|
|
40
|
+
|
|
41
|
+
- `groupBy(fn: (cols) => ExprUnit[]): Queryable<TData, never>` — GROUP BY. 이후 CUD 불가. count() 전이면 `wrap()` 필요.
|
|
42
|
+
- `having(predicate: (cols) => WhereExprUnit[]): Queryable<TData, never>` — GROUP BY 이후 필터. 여러 번 호출 시 AND 누적.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
db.order()
|
|
46
|
+
.select((o) => ({ userId: o.userId, total: expr.sum(o.amount) }))
|
|
47
|
+
.groupBy((o) => [o.userId])
|
|
48
|
+
.having((o) => [expr.gte(o.total, 10000)])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 조인 (join / joinSingle / include)
|
|
52
|
+
|
|
53
|
+
- `join(as, fn): Queryable<TData & { [as]?: R[] }>` — 1:N LEFT JOIN, 결과에 배열로 추가. `as`=속성 이름. `fn=(qr, cols) => qr.from(Table).where(...)` 로 조인 조건 작성(`qr` 은 `JoinQueryable`).
|
|
54
|
+
- `joinSingle(as, fn): Queryable<... & { [as]?: R }>` — N:1/1:1 LEFT JOIN, 단일 객체로 추가. `as` 동일 키면 덮어씀.
|
|
55
|
+
- `include(fn: (item: PathProxy) => PathProxy): Queryable` — 빌더 관계 기반 자동 조인. `fn` 은 PathProxy 로 관계 경로 선택(`(p) => p.user.company` 처럼 중첩 가능, 컬럼 필드는 접근 불가=컴파일 에러). FK→단일, FKTarget→배열(single 이면 단일). 관계 미정의·TableBuilder 아님이면 throw. 같은 경로 중복 호출은 무시.
|
|
56
|
+
|
|
57
|
+
`JoinQueryable`(join 콜백의 `qr`): `.from(Table)` 조인 대상 지정, `.select(columns)` 커스텀 컬럼, `.union(...queries)` 2개 이상 UNION(미만이면 throw).
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
db.post().joinSingle("user", (qr, p) => qr.from(User).where((u) => [expr.eq(u.id, p.userId)]))
|
|
61
|
+
db.user().include((u) => u.company).include((u) => u.posts)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 서브쿼리·결합 (wrap / union / recursive)
|
|
65
|
+
|
|
66
|
+
- `wrap(): Queryable<TData, never>` — 현재 쿼리를 서브쿼리로 래핑(새 alias). distinct()/groupBy() 후 count() 하기 전 필수.
|
|
67
|
+
- `static Queryable.union(...queries): Queryable` — 2개 이상 Queryable 을 UNION(중복 제거). 미만이면 throw. 첫 쿼리의 컬럼 구조 기준.
|
|
68
|
+
- `recursive(fn: (cte) => Queryable): Queryable<TData, never>` — 재귀 CTE. base 쿼리(현재)+`fn` 이 정의한 재귀부. `cte` 는 `RecursiveQueryable`: `.from(Table)`/`.select(cols)`/`.union(...)` 제공하며 자기참조용 `self` 속성을 결과에 부여(`e.self[0].id`). 계층 데이터(조직도·트리)에.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
db.employee()
|
|
72
|
+
.where((e) => [expr.null(e.managerId)])
|
|
73
|
+
.recursive((cte) => cte.from(Employee).where((e) => [expr.eq(e.managerId, e.self[0].id)]))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## SELECT 실행
|
|
77
|
+
|
|
78
|
+
- `execute(): Promise<TData[]>` — SELECT 실행, 행 배열 반환.
|
|
79
|
+
- `single(): Promise<TData | undefined>` — 단일 결과. 2건 이상이면 throw.
|
|
80
|
+
- `first(): Promise<TData | undefined>` — 첫 결과만(`top(1)`).
|
|
81
|
+
- `count(fn?): Promise<number>` — 행 수. `fn` 지정 시 해당 컬럼 COUNT. distinct()/groupBy() 직후 호출하면 throw(`wrap()` 먼저). 결과 없으면 0.
|
|
82
|
+
- `exists(): Promise<boolean>` — 조건 충족 행 존재 여부(`top(1)` 길이).
|
|
83
|
+
- `getSelectQueryDef(): SelectQueryDef` / `getResultMeta(outputColumns?): ResultMeta` — 실행 없이 AST·결과 메타 생성(executor·서브쿼리 내부에서 사용).
|
|
84
|
+
|
|
85
|
+
## INSERT
|
|
86
|
+
|
|
87
|
+
- `insert(records): Promise<void>` / `insert(records, outputColumns): Promise<Pick<...>[]>` — 레코드 배열 삽입. MSSQL 1000행 제한 대응 1000개씩 청크 분할. `outputColumns` 지정 시 삽입된 컬럼 반환. AI 컬럼에 명시값 있으면 overrideIdentity 자동 설정. 빈 배열은 무동작.
|
|
88
|
+
- `insertIfNotExists(record[, outputColumns])` — WHERE 조건에 일치하는 데이터 없을 때만 단건 삽입. 현재 체인의 where 가 존재 검사 조건.
|
|
89
|
+
- `insertInto(targetTable[, outputColumns])` — 현재 SELECT 결과를 다른 테이블에 INSERT INTO ... SELECT. `targetTable` 컬럼이 현재 데이터 형태와 호환(`DataToColumnBuilderRecord`)이어야 함.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const [row] = await db.user().insert([{ name: "홍길동" }], ["id"]);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## UPDATE / DELETE
|
|
96
|
+
|
|
97
|
+
- `update(recordFwd[, outputColumns])` — `recordFwd=(cols) => ({ col: ExprInput })` 로 갱신값 지정(기존 값 참조 가능: `expr.mul(p.price, 1.1)`). where 로 대상 한정. `outputColumns` 시 갱신 행 반환.
|
|
98
|
+
- `delete([outputColumns])` — where 조건 행 삭제. `outputColumns` 시 삭제된 행 반환.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
await db.user().where((u) => [expr.eq(u.id, 1)]).update((u) => ({ name: expr.val("string", "새이름") }));
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## UPSERT
|
|
105
|
+
|
|
106
|
+
- `upsert(updateFn[, insertFn][, outputColumns])` — where 일치 시 UPDATE, 없으면 INSERT(MERGE 패턴). `updateFn=(cols) => 갱신값`. `insertFn=(updateRecord) => 삽입값`(생략 시 update 값 재사용, updateRecord 를 받아 추가 컬럼 합치기 가능). `outputColumns` 시 영향 행 반환.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
await db.user()
|
|
110
|
+
.where((u) => [expr.eq(u.email, "t@t.com")])
|
|
111
|
+
.upsert(() => ({ loginCount: expr.val("number", 1) }), (up) => ({ ...up, email: expr.val("string", "t@t.com") }));
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`get...QueryDef` 류(`getInsertQueryDef`/`getUpdateQueryDef`/`getUpsertQueryDef` 등)는 실행 없이 AST 반환. `switchFk(enabled)` 는 이 테이블 FK 제약 on/off(Table/View 기반 아니면 throw).
|
|
115
|
+
|
|
116
|
+
## 결과 타입 유틸
|
|
117
|
+
|
|
118
|
+
- `type QueryableRecord<TData>` — 각 컬럼을 `ExprUnit` 으로, 중첩 관계를 재귀 래핑한 콜백 인자 타입.
|
|
119
|
+
- `type QueryableWriteRecord<TData>` — 컬럼을 `ExprInput`(쓰기) 으로 매핑(update/upsert 입력).
|
|
120
|
+
- `type UnwrapQueryableRecord<R>` — select 결과 구조를 데이터 타입으로 역변환(ExprUnit→값).
|
|
121
|
+
- `type PathProxy<TObject>` — include 의 타입 안전 경로 프록시(관계 필드만 노출).
|
|
122
|
+
- `getMatchedPrimaryKeys(fkCols, targetTable): string[]` — FK 컬럼 수와 대상 PK 를 매칭해 PK 이름 배열 반환. 개수 불일치 시 throw. include 내부 조인 조건 생성에 사용.
|
|
123
|
+
|
|
124
|
+
## 프로시저 실행 (Executable / executable)
|
|
125
|
+
|
|
126
|
+
`DbContext.executable(Procedure)` 가 `() => Executable` 팩토리를 반환한다. `Executable<TParams, TReturns>` 는 프로시저 실행 래퍼.
|
|
127
|
+
|
|
128
|
+
- `executable(db: DbContextBase, builder: ProcedureBuilder): () => Executable` — 팩토리(보통 `DbContext.executable()` 보호 메서드가 호출).
|
|
129
|
+
- `Executable.execute(params): Promise<TReturns[][]>` — 실행. `params` 는 `ProcedureBuilder.params()` 키별 값(리터럴 또는 ExprUnit). 결과는 결과셋 배열의 배열(다중 SELECT 대응).
|
|
130
|
+
- `Executable.getExecProcQueryDef(params?): ExecProcQueryDef` — 실행 AST 생성. 파라미터 미정의 프로시저에 값 전달 시 throw.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const [rows] = await db.getUserById().execute({ userId: 1 });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 검색 파서 (parseSearchQuery)
|
|
137
|
+
|
|
138
|
+
`Queryable.search()` 내부에서 검색 문법 문자열을 SQL LIKE 패턴으로 변환. 직접 호출도 가능.
|
|
139
|
+
|
|
140
|
+
- `parseSearchQuery(searchText: string): ParsedSearchQuery` — `{ or, must, not }`(각각 LIKE 패턴 배열) 반환.
|
|
141
|
+
- `or: string[]` — 공백 구분 일반 토큰(OR, 하나 이상 일치).
|
|
142
|
+
- `must: string[]` — `+토큰` 또는 `"정확한 구문"`(AND, 필수 포함).
|
|
143
|
+
- `not: string[]` — `-토큰`(NOT, 제외).
|
|
144
|
+
- 문법: `term1 term2`(OR), `+term`(필수), `-term`(제외), `"구문"`(정확·필수), `*`(와일드카드→`%`). 와일드카드 없는 토큰은 `%토큰%`(부분 일치)로 변환. 이스케이프 `\\ \* \% \" \+ \-`. 닫히지 않은 따옴표는 일반 텍스트 처리.
|
|
145
|
+
- `interface ParsedSearchQuery` — 위 `or`/`must`/`not` 세 패턴 배열을 담는 타입.
|