@things-factory/shell 10.1.121 → 10.1.123
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/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/typeorm/money-column.d.ts +34 -0
- package/dist-server/typeorm/money-column.js +43 -0
- package/dist-server/typeorm/money-column.js.map +1 -0
- package/package.json +3 -3
- package/tests/money-column.test.ts +34 -0
- package/tests/styles-sources-load.test.ts +75 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ColumnOptions, ValueTransformer } from 'typeorm';
|
|
2
|
+
/**
|
|
3
|
+
* `decimal` 컬럼을 숫자로 읽고 쓰는 변환기.
|
|
4
|
+
*
|
|
5
|
+
* Postgres·MySQL 은 `decimal` 을 **문자열**로 돌려준다(정밀도를 지키려고). 그대로 두면
|
|
6
|
+
* GraphQL `Float` 필드와 합산 코드가 `'1200.00' + 300` 같은 글자 이어붙이기를 한다.
|
|
7
|
+
* 그래서 읽을 때 숫자로 바꾼다. 비어 있음(`null`·`undefined`)은 그대로 둔다 — 0 으로 만들지 않는다.
|
|
8
|
+
*/
|
|
9
|
+
export declare const decimalTransformer: ValueTransformer;
|
|
10
|
+
/**
|
|
11
|
+
* 금액 컬럼 — `decimal(18, 2)`.
|
|
12
|
+
*
|
|
13
|
+
* ## 왜 `decimal` 인가
|
|
14
|
+
*
|
|
15
|
+
* - 형식을 적지 않은 `number` 컬럼은 32비트 정수가 된다. 원 단위로는 정확하지만 한 칸에
|
|
16
|
+
* 약 21억까지이고 소수(센트)를 못 담는다.
|
|
17
|
+
* - `float` 은 0.1 을 정확히 담지 못한다. 수천 줄을 더하면 은행 잔액과 어긋난다.
|
|
18
|
+
* - `decimal` 은 SQL 표준이라 다섯 드라이버(postgres·mysql/mariadb·sqlite·mssql·oracle)가 모두 받는다.
|
|
19
|
+
* 18자리 중 소수 2자리 — 원화 금액 전부와 외화 센트를 담는다.
|
|
20
|
+
*
|
|
21
|
+
* sqlite 는 자릿수를 강제하지 않는다(NUMERIC 친화형). 정수 원화는 정확하지만 소수 금액은
|
|
22
|
+
* 내부에서 실수로 담길 수 있다 — 소수 금액의 정확성은 서버 DB 를 전제로 한다.
|
|
23
|
+
*
|
|
24
|
+
* ## 쓰는 법
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* @Column({ nullable: true, ...moneyColumn() })
|
|
28
|
+
* @Field(type => Float, { nullable: true })
|
|
29
|
+
* supplyAmount?: number
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* `nullable`·`default` 같은 나머지 옵션은 호출하는 쪽이 정한다. GraphQL 필드는 `Float` 로 둔다.
|
|
33
|
+
*/
|
|
34
|
+
export declare function moneyColumn(options?: ColumnOptions): ColumnOptions;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decimalTransformer = void 0;
|
|
4
|
+
exports.moneyColumn = moneyColumn;
|
|
5
|
+
/**
|
|
6
|
+
* `decimal` 컬럼을 숫자로 읽고 쓰는 변환기.
|
|
7
|
+
*
|
|
8
|
+
* Postgres·MySQL 은 `decimal` 을 **문자열**로 돌려준다(정밀도를 지키려고). 그대로 두면
|
|
9
|
+
* GraphQL `Float` 필드와 합산 코드가 `'1200.00' + 300` 같은 글자 이어붙이기를 한다.
|
|
10
|
+
* 그래서 읽을 때 숫자로 바꾼다. 비어 있음(`null`·`undefined`)은 그대로 둔다 — 0 으로 만들지 않는다.
|
|
11
|
+
*/
|
|
12
|
+
exports.decimalTransformer = {
|
|
13
|
+
to: (value) => (value === undefined ? undefined : value === null || value === '' ? null : Number(value)),
|
|
14
|
+
from: (value) => (value === null || value === undefined ? value : Number(value))
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 금액 컬럼 — `decimal(18, 2)`.
|
|
18
|
+
*
|
|
19
|
+
* ## 왜 `decimal` 인가
|
|
20
|
+
*
|
|
21
|
+
* - 형식을 적지 않은 `number` 컬럼은 32비트 정수가 된다. 원 단위로는 정확하지만 한 칸에
|
|
22
|
+
* 약 21억까지이고 소수(센트)를 못 담는다.
|
|
23
|
+
* - `float` 은 0.1 을 정확히 담지 못한다. 수천 줄을 더하면 은행 잔액과 어긋난다.
|
|
24
|
+
* - `decimal` 은 SQL 표준이라 다섯 드라이버(postgres·mysql/mariadb·sqlite·mssql·oracle)가 모두 받는다.
|
|
25
|
+
* 18자리 중 소수 2자리 — 원화 금액 전부와 외화 센트를 담는다.
|
|
26
|
+
*
|
|
27
|
+
* sqlite 는 자릿수를 강제하지 않는다(NUMERIC 친화형). 정수 원화는 정확하지만 소수 금액은
|
|
28
|
+
* 내부에서 실수로 담길 수 있다 — 소수 금액의 정확성은 서버 DB 를 전제로 한다.
|
|
29
|
+
*
|
|
30
|
+
* ## 쓰는 법
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* @Column({ nullable: true, ...moneyColumn() })
|
|
34
|
+
* @Field(type => Float, { nullable: true })
|
|
35
|
+
* supplyAmount?: number
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* `nullable`·`default` 같은 나머지 옵션은 호출하는 쪽이 정한다. GraphQL 필드는 `Float` 로 둔다.
|
|
39
|
+
*/
|
|
40
|
+
function moneyColumn(options = {}) {
|
|
41
|
+
return { type: 'decimal', precision: 18, scale: 2, transformer: exports.decimalTransformer, ...options };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=money-column.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money-column.js","sourceRoot":"","sources":["../../server/typeorm/money-column.ts"],"names":[],"mappings":";;;AAsCA,kCAEC;AAtCD;;;;;;GAMG;AACU,QAAA,kBAAkB,GAAqB;IAClD,EAAE,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjH,IAAI,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;CAC1F,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAgB,WAAW,CAAC,UAAyB,EAAE;IACrD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,0BAAkB,EAAE,GAAG,OAAO,EAAE,CAAA;AAClG,CAAC","sourcesContent":["import { ColumnOptions, ValueTransformer } from 'typeorm'\n\n/**\n * `decimal` 컬럼을 숫자로 읽고 쓰는 변환기.\n *\n * Postgres·MySQL 은 `decimal` 을 **문자열**로 돌려준다(정밀도를 지키려고). 그대로 두면\n * GraphQL `Float` 필드와 합산 코드가 `'1200.00' + 300` 같은 글자 이어붙이기를 한다.\n * 그래서 읽을 때 숫자로 바꾼다. 비어 있음(`null`·`undefined`)은 그대로 둔다 — 0 으로 만들지 않는다.\n */\nexport const decimalTransformer: ValueTransformer = {\n to: (value: unknown) => (value === undefined ? undefined : value === null || value === '' ? null : Number(value)),\n from: (value: unknown) => (value === null || value === undefined ? value : Number(value))\n}\n\n/**\n * 금액 컬럼 — `decimal(18, 2)`.\n *\n * ## 왜 `decimal` 인가\n *\n * - 형식을 적지 않은 `number` 컬럼은 32비트 정수가 된다. 원 단위로는 정확하지만 한 칸에\n * 약 21억까지이고 소수(센트)를 못 담는다.\n * - `float` 은 0.1 을 정확히 담지 못한다. 수천 줄을 더하면 은행 잔액과 어긋난다.\n * - `decimal` 은 SQL 표준이라 다섯 드라이버(postgres·mysql/mariadb·sqlite·mssql·oracle)가 모두 받는다.\n * 18자리 중 소수 2자리 — 원화 금액 전부와 외화 센트를 담는다.\n *\n * sqlite 는 자릿수를 강제하지 않는다(NUMERIC 친화형). 정수 원화는 정확하지만 소수 금액은\n * 내부에서 실수로 담길 수 있다 — 소수 금액의 정확성은 서버 DB 를 전제로 한다.\n *\n * ## 쓰는 법\n *\n * ```ts\n * @Column({ nullable: true, ...moneyColumn() })\n * @Field(type => Float, { nullable: true })\n * supplyAmount?: number\n * ```\n *\n * `nullable`·`default` 같은 나머지 옵션은 호출하는 쪽이 정한다. GraphQL 필드는 `Float` 로 둔다.\n */\nexport function moneyColumn(options: ColumnOptions = {}): ColumnOptions {\n return { type: 'decimal', precision: 18, scale: 2, transformer: decimalTransformer, ...options }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.123",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@things-factory/ejs-remote": "^10.1.20",
|
|
57
57
|
"@things-factory/env": "^10.1.20",
|
|
58
58
|
"@things-factory/operato-license-checker": "^4.0.4",
|
|
59
|
-
"@things-factory/styles": "^10.1.
|
|
59
|
+
"@things-factory/styles": "^10.1.123",
|
|
60
60
|
"@things-factory/utils": "^10.1.20",
|
|
61
61
|
"@types/koa": "^3.0.1",
|
|
62
62
|
"@types/koa-bodyparser": "^4.3.13",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"pg": "^8.7.3",
|
|
127
127
|
"sqlite3": "^5.0.8"
|
|
128
128
|
},
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "9781f1eb37a7e6a37285878cff16f8683747eb7a"
|
|
130
130
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { decimalTransformer, moneyColumn } from '../server/typeorm/money-column'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Money is a `decimal(18, 2)` column read back as a number.
|
|
5
|
+
*
|
|
6
|
+
* Postgres and MySQL hand `decimal` back as a string. Left alone, a sum over rows becomes string
|
|
7
|
+
* concatenation ('1200.00' + 300 = '1200.00300') and the GraphQL Float field receives text.
|
|
8
|
+
*/
|
|
9
|
+
describe('a money column', () => {
|
|
10
|
+
it('is decimal(18, 2) with the number transformer', () => {
|
|
11
|
+
expect(moneyColumn()).toEqual({ type: 'decimal', precision: 18, scale: 2, transformer: decimalTransformer })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('keeps the options the caller gives', () => {
|
|
15
|
+
expect(moneyColumn({ nullable: true, default: 0 })).toMatchObject({ type: 'decimal', nullable: true, default: 0 })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('reads a driver string as a number', () => {
|
|
19
|
+
expect(decimalTransformer.from('1200.00')).toBe(1200)
|
|
20
|
+
expect(decimalTransformer.from('-71200000.50')).toBe(-71200000.5)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('leaves an empty value empty, never 0', () => {
|
|
24
|
+
expect(decimalTransformer.from(null)).toBeNull()
|
|
25
|
+
expect(decimalTransformer.from(undefined)).toBeUndefined()
|
|
26
|
+
expect(decimalTransformer.to(null)).toBeNull()
|
|
27
|
+
expect(decimalTransformer.to('')).toBeNull()
|
|
28
|
+
expect(decimalTransformer.to(undefined)).toBeUndefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('writes a number given as text as a number', () => {
|
|
32
|
+
expect(decimalTransformer.to('658630')).toBe(658630)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process'
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import ts from 'typescript'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* @things-factory/styles is published as source: no build, no compiler. A backtick in a comment inside a
|
|
8
|
+
* css`` template ends the template early, and nothing catches it until an app bundles the file -
|
|
9
|
+
* 10.1.121 shipped that way (surface-styles.js, a path written in backticks in a comment), and every
|
|
10
|
+
* app that loads the package failed to bundle (2026-10-05, ADR-0117).
|
|
11
|
+
*
|
|
12
|
+
* Each style file is loaded by node on its own. index.js is left out: it imports without extensions,
|
|
13
|
+
* which the bundler resolves and node does not.
|
|
14
|
+
*/
|
|
15
|
+
const SRC = path.resolve(__dirname, '../../styles/src')
|
|
16
|
+
|
|
17
|
+
const styleFiles = () => readdirSync(SRC).filter(name => name.endsWith('.js') && name !== 'index.js')
|
|
18
|
+
|
|
19
|
+
describe('@things-factory/styles sources', () => {
|
|
20
|
+
it('has style files to load - an empty list would make the check below pass on nothing', () => {
|
|
21
|
+
expect(styleFiles().length).toBeGreaterThan(3)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it.each(styleFiles())('%s loads as a module', name => {
|
|
25
|
+
const file = path.join(SRC, name)
|
|
26
|
+
const run = spawnSync(process.execPath, ['--input-type=module', '-e', `await import(${JSON.stringify(file)})`], {
|
|
27
|
+
cwd: SRC,
|
|
28
|
+
encoding: 'utf8'
|
|
29
|
+
})
|
|
30
|
+
expect(run.stderr.split('\n').filter(line => /Error|Unexpected/.test(line))).toEqual([])
|
|
31
|
+
expect(run.status).toBe(0)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* The same hole elsewhere (counted 2026-10-05): files that go out with no compiler reading them before
|
|
37
|
+
* publish - component-ui has no build script at all (its .ts is compiled only inside each app's bundle),
|
|
38
|
+
* and two .js entries that the packages' tsc does not read. They are parsed here for syntax; a template
|
|
39
|
+
* cut short by a backtick is a syntax error to the parser.
|
|
40
|
+
*/
|
|
41
|
+
const PACKAGES = path.resolve(__dirname, '../..')
|
|
42
|
+
|
|
43
|
+
function filesUnder(dir: string): string[] {
|
|
44
|
+
const out: string[] = []
|
|
45
|
+
for (const name of readdirSync(dir)) {
|
|
46
|
+
if (name === 'node_modules' || name.startsWith('dist')) continue
|
|
47
|
+
const full = path.join(dir, name)
|
|
48
|
+
if (statSync(full).isDirectory()) out.push(...filesUnder(full))
|
|
49
|
+
else if (/\.(ts|js)$/.test(name) && !name.endsWith('.d.ts')) out.push(full)
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const UNCOMPILED = [
|
|
55
|
+
...filesUnder(path.join(PACKAGES, 'component-ui/client')),
|
|
56
|
+
path.join(PACKAGES, 'board-service/client/entries/headless-scene-components.js'),
|
|
57
|
+
path.join(PACKAGES, 'shell/client/entries/public/home.js')
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
describe('client files no compiler reads before publish', () => {
|
|
61
|
+
it('found the files - an empty list would pass on nothing', () => {
|
|
62
|
+
expect(UNCOMPILED.length).toBeGreaterThan(5)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it.each(UNCOMPILED.map(file => path.relative(PACKAGES, file)))('%s parses', relative => {
|
|
66
|
+
const file = path.join(PACKAGES, relative)
|
|
67
|
+
const result = ts.transpileModule(readFileSync(file, 'utf8'), {
|
|
68
|
+
fileName: file,
|
|
69
|
+
reportDiagnostics: true,
|
|
70
|
+
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, experimentalDecorators: true, allowJs: true }
|
|
71
|
+
})
|
|
72
|
+
const syntax = (result.diagnostics ?? []).map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
|
|
73
|
+
expect(syntax).toEqual([])
|
|
74
|
+
})
|
|
75
|
+
})
|