@things-factory/shell 10.1.188 → 10.1.206
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/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/calendar-day.d.ts +12 -0
- package/dist-server/utils/calendar-day.js +21 -0
- package/dist-server/utils/calendar-day.js.map +1 -0
- package/dist-server/utils/condition-builder.js +7 -19
- package/dist-server/utils/condition-builder.js.map +1 -1
- package/dist-server/utils/get-query-builder-from-list-params.js +4 -14
- package/dist-server/utils/get-query-builder-from-list-params.js.map +1 -1
- 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/dist-server/utils/membership-clause.d.ts +19 -0
- package/dist-server/utils/membership-clause.js +30 -0
- package/dist-server/utils/membership-clause.js.map +1 -0
- package/package.json +2 -2
- package/tests/calendar-day.test.ts +21 -0
- package/tests/empty-membership.test.ts +117 -0
- package/translations/en.json +2 -1
- package/translations/es.json +2 -1
- package/translations/ja.json +2 -1
- package/translations/ko.json +2 -1
- package/translations/ms.json +2 -1
- package/translations/zh.json +2 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { Column, DataSource, Entity, PrimaryColumn, Repository } from 'typeorm'
|
|
3
|
+
|
|
4
|
+
import { buildCondition } from '../server/utils/condition-builder.js'
|
|
5
|
+
import { getQueryBuilderFromListParams } from '../server/utils/get-query-builder-from-list-params.js'
|
|
6
|
+
import { ListParam } from '../server/service/common-types/list-param.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A list membership filter with an empty list: `in []` matches nothing, `notin []` and `notin_with_null []`
|
|
10
|
+
* constrain nothing.
|
|
11
|
+
*
|
|
12
|
+
* The board list once sent `state in []`. Both builders wrote it as `IN ()`, which postgres refuses as a syntax
|
|
13
|
+
* error, so the whole list failed in production. sqlite accepts `IN ()`, so a sqlite run alone never sees it: the
|
|
14
|
+
* postgres SQL is checked from the dialect without a server, and run on a real one when SHELL_PG_URL names it:
|
|
15
|
+
*
|
|
16
|
+
* docker run -d --name shell-pg -e POSTGRES_PASSWORD=shell -e POSTGRES_USER=shell -e POSTGRES_DB=shell \
|
|
17
|
+
* -p 55434:5432 postgres:14-alpine
|
|
18
|
+
* SHELL_PG_URL=postgres://shell:shell@localhost:55434/shell npx jest tests/empty-membership.test.ts
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
@Entity()
|
|
22
|
+
class Gadget {
|
|
23
|
+
@PrimaryColumn({ type: 'integer' })
|
|
24
|
+
id!: number
|
|
25
|
+
|
|
26
|
+
@Column({ type: 'text', nullable: true })
|
|
27
|
+
state!: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ROWS = [
|
|
31
|
+
{ id: 1, state: 'draft' },
|
|
32
|
+
{ id: 2, state: 'released' },
|
|
33
|
+
{ id: 3, state: null }
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const CASES: [operator: string, expected: number[]][] = [
|
|
37
|
+
['in', []],
|
|
38
|
+
['notin', [1, 2, 3]],
|
|
39
|
+
['notin_with_null', [1, 2, 3]]
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function listParamsQuery(repository: Repository<Gadget>, operator: string, value: any) {
|
|
43
|
+
const params = { filters: [{ name: 'state', operator, value }], pagination: { page: 1, limit: 10 } } as unknown as ListParam
|
|
44
|
+
return getQueryBuilderFromListParams({ repository, params })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function conditionQuery(repository: Repository<Gadget>, operator: string, value: any) {
|
|
48
|
+
const queryBuilder = repository.createQueryBuilder('gadget')
|
|
49
|
+
const { clause, parameters } = buildCondition('gadget', 'state', operator, value, false, 0) as any
|
|
50
|
+
return queryBuilder.andWhere(clause, parameters)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BUILDERS = [
|
|
54
|
+
['getQueryBuilderFromListParams', listParamsQuery],
|
|
55
|
+
['buildCondition', conditionQuery]
|
|
56
|
+
] as const
|
|
57
|
+
|
|
58
|
+
async function idsOf(query: ReturnType<typeof conditionQuery>): Promise<number[]> {
|
|
59
|
+
return (await query.getMany()).map(row => row.id).sort((a, b) => a - b)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** What each builder reads from the same rows on a database that runs the SQL. */
|
|
63
|
+
function readsOn(describeOn: jest.Describe, title: string, options: () => any) {
|
|
64
|
+
describeOn(title, () => {
|
|
65
|
+
let dataSource: DataSource
|
|
66
|
+
let repository: Repository<Gadget>
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
dataSource = new DataSource({ ...options(), synchronize: true, entities: [Gadget] })
|
|
70
|
+
await dataSource.initialize()
|
|
71
|
+
repository = dataSource.getRepository(Gadget)
|
|
72
|
+
await repository.save(ROWS)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await dataSource?.destroy()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe.each(BUILDERS)('%s', (_, query) => {
|
|
80
|
+
it.each(CASES)('%s [] reads %j', async (operator, expected) => {
|
|
81
|
+
expect(await idsOf(query(repository, operator, []))).toEqual(expected)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('a list with a choice still filters by it', async () => {
|
|
85
|
+
expect(await idsOf(query(repository, 'in', ['draft']))).toEqual([1])
|
|
86
|
+
expect(await idsOf(query(repository, 'notin', ['draft']))).toEqual([2])
|
|
87
|
+
expect(await idsOf(query(repository, 'notin_with_null', ['draft']))).toEqual([2, 3])
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
readsOn(describe, 'an empty membership list on sqlite', () => ({ type: 'better-sqlite3', database: ':memory:' }))
|
|
94
|
+
|
|
95
|
+
describe('an empty membership list in postgres SQL, without a server', () => {
|
|
96
|
+
let repository: Repository<Gadget>
|
|
97
|
+
|
|
98
|
+
beforeAll(async () => {
|
|
99
|
+
const dataSource = new DataSource({ type: 'postgres', host: 'localhost', database: 'unused', entities: [Gadget] })
|
|
100
|
+
await (dataSource as any).buildMetadatas()
|
|
101
|
+
repository = dataSource.getRepository(Gadget)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe.each(BUILDERS)('%s', (_, query) => {
|
|
105
|
+
it.each(CASES)('%s [] writes no empty IN list', operator => {
|
|
106
|
+
const [sql] = query(repository, operator, []).getQueryAndParameters()
|
|
107
|
+
expect(sql).not.toMatch(/IN \(\s*\)/)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const url = process.env.SHELL_PG_URL
|
|
113
|
+
readsOn(url ? describe : describe.skip, 'an empty membership list on postgres', () => ({
|
|
114
|
+
type: 'postgres',
|
|
115
|
+
url,
|
|
116
|
+
dropSchema: true
|
|
117
|
+
}))
|
package/translations/en.json
CHANGED
|
@@ -185,5 +185,6 @@
|
|
|
185
185
|
"button.resume-editing": "resume editing",
|
|
186
186
|
"button.discard": "discard",
|
|
187
187
|
"figure.label.motion": "Motion",
|
|
188
|
-
"figure.text.preview-needs-a-part": "add a part to see how it looks placed on a drawing."
|
|
188
|
+
"figure.text.preview-needs-a-part": "add a part to see how it looks placed on a drawing.",
|
|
189
|
+
"error.timezone-required": "The $t(term.domain) has no timezone. Set it in $t(term.domain) management."
|
|
189
190
|
}
|
package/translations/es.json
CHANGED
|
@@ -185,5 +185,6 @@
|
|
|
185
185
|
"button.resume-editing": "reanudar edición",
|
|
186
186
|
"button.discard": "descartar",
|
|
187
187
|
"figure.label.motion": "Movimiento",
|
|
188
|
-
"figure.text.preview-needs-a-part": "añada una pieza para ver cómo se ve colocada en un plano."
|
|
188
|
+
"figure.text.preview-needs-a-part": "añada una pieza para ver cómo se ve colocada en un plano.",
|
|
189
|
+
"error.timezone-required": "El dominio no tiene zona horaria. Configúrela en la gestión de dominios."
|
|
189
190
|
}
|
package/translations/ja.json
CHANGED
|
@@ -185,5 +185,6 @@
|
|
|
185
185
|
"button.resume-editing": "編集を続ける",
|
|
186
186
|
"button.discard": "破棄",
|
|
187
187
|
"figure.label.motion": "Motion",
|
|
188
|
-
"figure.text.preview-needs-a-part": "部品を追加すると、図面に置いたときの姿が表示されます。"
|
|
188
|
+
"figure.text.preview-needs-a-part": "部品を追加すると、図面に置いたときの姿が表示されます。",
|
|
189
|
+
"error.timezone-required": "事業所のタイムゾーンが設定されていません。事業所管理でタイムゾーンを設定してください。"
|
|
189
190
|
}
|
package/translations/ko.json
CHANGED
|
@@ -185,5 +185,6 @@
|
|
|
185
185
|
"button.resume-editing": "이어서 편집",
|
|
186
186
|
"button.discard": "버리기",
|
|
187
187
|
"figure.label.motion": "움직임",
|
|
188
|
-
"figure.text.preview-needs-a-part": "부품을 추가하면 도면에 놓았을 때의 모습이 표시됩니다."
|
|
188
|
+
"figure.text.preview-needs-a-part": "부품을 추가하면 도면에 놓았을 때의 모습이 표시됩니다.",
|
|
189
|
+
"error.timezone-required": "$t(term.domain) 시간대가 정해지지 않았습니다. $t(term.domain) 관리에서 시간대를 정하세요."
|
|
189
190
|
}
|
package/translations/ms.json
CHANGED
|
@@ -185,5 +185,6 @@
|
|
|
185
185
|
"button.resume-editing": "sambung menyunting",
|
|
186
186
|
"button.discard": "buang",
|
|
187
187
|
"figure.label.motion": "Motion",
|
|
188
|
-
"figure.text.preview-needs-a-part": "tambah bahagian untuk melihat rupanya apabila diletakkan pada lukisan."
|
|
188
|
+
"figure.text.preview-needs-a-part": "tambah bahagian untuk melihat rupanya apabila diletakkan pada lukisan.",
|
|
189
|
+
"error.timezone-required": "Domain tiada zon waktu. Tetapkannya dalam pengurusan domain."
|
|
189
190
|
}
|
package/translations/zh.json
CHANGED
|
@@ -185,5 +185,6 @@
|
|
|
185
185
|
"button.resume-editing": "继续编辑",
|
|
186
186
|
"button.discard": "放弃",
|
|
187
187
|
"figure.label.motion": "Motion",
|
|
188
|
-
"figure.text.preview-needs-a-part": "添加部件后会显示放置到图面上的效果。"
|
|
188
|
+
"figure.text.preview-needs-a-part": "添加部件后会显示放置到图面上的效果。",
|
|
189
|
+
"error.timezone-required": "尚未设置单位时区。请在单位管理中设置时区。"
|
|
189
190
|
}
|