@things-factory/ai-assistant 10.1.179 → 10.1.183
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-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/data-read/list-read.d.ts +2 -2
- package/dist-server/service/data-read/list-read.js +5 -7
- package/dist-server/service/data-read/list-read.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +2 -0
- package/dist-server/service/data-read/read-target.js +11 -3
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/server/service/data-read/data-read.test.ts +41 -14
- package/server/service/data-read/list-read.ts +7 -9
- package/server/service/data-read/read-target.ts +15 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.183",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@apollo/client": "^3.13.0",
|
|
29
29
|
"@material/web": "^2.0.0",
|
|
30
|
-
"@operato/graphql": "^10.
|
|
30
|
+
"@operato/graphql": "^10.29.39",
|
|
31
31
|
"@operato/i18n": "^10.0.0",
|
|
32
|
-
"@operato/input": "^10.29.
|
|
33
|
-
"@operato/layout": "^10.
|
|
34
|
-
"@operato/popup": "^10.
|
|
35
|
-
"@operato/shell": "^10.
|
|
32
|
+
"@operato/input": "^10.29.39",
|
|
33
|
+
"@operato/layout": "^10.29.39",
|
|
34
|
+
"@operato/popup": "^10.29.39",
|
|
35
|
+
"@operato/shell": "^10.29.39",
|
|
36
36
|
"@operato/styles": "^10.0.0",
|
|
37
|
-
"@operato/utils": "^10.29.
|
|
38
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
37
|
+
"@operato/utils": "^10.29.39",
|
|
38
|
+
"@things-factory/ai-client-base": "^10.1.183",
|
|
39
39
|
"@things-factory/apptool-base": "^10.1.179",
|
|
40
|
-
"@things-factory/attachment-base": "^10.1.
|
|
41
|
-
"@things-factory/auth-base": "^10.1.
|
|
40
|
+
"@things-factory/attachment-base": "^10.1.183",
|
|
41
|
+
"@things-factory/auth-base": "^10.1.183",
|
|
42
42
|
"@things-factory/env": "^10.1.149",
|
|
43
|
-
"@things-factory/setting-base": "^10.1.
|
|
43
|
+
"@things-factory/setting-base": "^10.1.183",
|
|
44
44
|
"@things-factory/shell": "^10.1.179",
|
|
45
45
|
"dompurify": "^3.0.0",
|
|
46
46
|
"exceljs": "4.2.0",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"copyfiles": "^2.4.1",
|
|
56
56
|
"rimraf": "^5.0.0"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "c36684e0db8353f5f4523ed55111df3154bae19e"
|
|
59
59
|
}
|
|
@@ -58,7 +58,7 @@ beforeEach(() => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
describe('reading a list', () => {
|
|
61
|
-
it(
|
|
61
|
+
it("★ the conditions become the application's filters - the model never names the query's columns", async () => {
|
|
62
62
|
const result: any = await queryRecords(target, { from: '2026-04-01', to: '2026-09-30', flow: 'EXPENSE' }, {}, run)
|
|
63
63
|
expect(calls[0].filters).toEqual([
|
|
64
64
|
{ name: 'transactionDate', operator: 'gte', value: '2026-04-01' },
|
|
@@ -124,7 +124,7 @@ describe('the screen card', () => {
|
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
describe('declaring and offering', () => {
|
|
127
|
-
it(
|
|
127
|
+
it("★ the tools are locked by the list query's door, and a mode opens them by key", () => {
|
|
128
128
|
const category = readCategoryOf(target, () => run)
|
|
129
129
|
expect(category.name).toBe(readCategoryName(target.key))
|
|
130
130
|
expect(category.specs.map(spec => spec.name)).toEqual([queryToolName(target.key), screenToolName(target.key)])
|
|
@@ -196,7 +196,7 @@ describe('a list declared in one line', () => {
|
|
|
196
196
|
narrowing: { year: 'year', month: 'month', flow: 'type' },
|
|
197
197
|
search: 'memo and counterparty',
|
|
198
198
|
route: 'cash-transaction-list',
|
|
199
|
-
|
|
199
|
+
screenQuery: true
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
it('★ becomes a target with the conditions its fields allow - and only those', async () => {
|
|
@@ -207,22 +207,44 @@ describe('a list declared in one line', () => {
|
|
|
207
207
|
const bare: ReadTarget = listReadTarget({ ...line, key: 'office.bare', narrowing: undefined, dateField: undefined, search: undefined, columns: [{ name: 'name', type: 'string' }] })
|
|
208
208
|
expect(bare.criteria).toEqual([])
|
|
209
209
|
expect(bare.query).not.toContain('$search')
|
|
210
|
-
expect(bare.screen!.takes).toEqual([])
|
|
210
|
+
expect(bare.screen!.takes).toEqual(['id'])
|
|
211
211
|
})
|
|
212
212
|
|
|
213
|
-
it('
|
|
213
|
+
it('the screen uses the same predicates as a read, including ranges outside its filter form', () => {
|
|
214
214
|
const made: ReadTarget = listReadTarget(line)
|
|
215
215
|
const to = made.criteria.find(one => one.name === 'to')!
|
|
216
|
-
expect(to.filter('2026-09-30')).toEqual({
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
expect(to.filter('2026-09-30')).toEqual({
|
|
217
|
+
name: 'transactionDate',
|
|
218
|
+
operator: 'lte',
|
|
219
|
+
value: '2026-09-30T23:59:59.999'
|
|
220
|
+
})
|
|
221
|
+
const criteria = { year: 2026, month: 9, flow: 'EXPENSE', text: '외주', from: '2026-01-01', minAmount: 100000 }
|
|
222
|
+
const address = screenAddress(made, criteria)!
|
|
223
|
+
const query = new URLSearchParams(address.href.split('?')[1])
|
|
224
|
+
expect(query.has('year')).toBe(false)
|
|
225
|
+
expect(query.has('month')).toBe(false)
|
|
226
|
+
expect(query.has('flow')).toBe(false)
|
|
227
|
+
expect(JSON.parse(query.get('filters')!)).toEqual([
|
|
228
|
+
{ name: 'transactionDate', operator: 'gte', value: '2026-01-01' },
|
|
229
|
+
{ name: 'year', operator: 'eq', value: 2026 },
|
|
230
|
+
{ name: 'month', operator: 'eq', value: 9 },
|
|
231
|
+
{ name: 'type', operator: 'eq', value: 'EXPENSE' },
|
|
232
|
+
{ name: 'amount', operator: 'gte', value: 100000 }
|
|
233
|
+
])
|
|
234
|
+
expect(query.get('search')).toBe('외주')
|
|
235
|
+
expect(address.notApplied).toEqual([])
|
|
236
|
+
const plain: ReadTarget = listReadTarget({ ...line, key: 'plant.x', screenQuery: false })
|
|
220
237
|
expect(plain.screen!.takes).toEqual([])
|
|
221
|
-
expect(plain
|
|
238
|
+
expect(screenAddress(plain, { year: 2026 })!.notApplied).toEqual(['year'])
|
|
222
239
|
})
|
|
223
240
|
|
|
224
|
-
it(
|
|
225
|
-
const made: ReadTarget = listReadTarget({
|
|
241
|
+
it("★ a fixed filter is on every read, out of the model's reach - a condition narrows the list, never widens it", async () => {
|
|
242
|
+
const made: ReadTarget = listReadTarget({
|
|
243
|
+
...line,
|
|
244
|
+
key: 'office.spending',
|
|
245
|
+
listQuery: 'cashTransactions',
|
|
246
|
+
fixedFilters: [{ name: 'type', operator: 'eq', value: 'EXPENSE' }]
|
|
247
|
+
})
|
|
226
248
|
declareReadTarget(made)
|
|
227
249
|
expect(made.criteria.map(one => one.name)).not.toContain('type')
|
|
228
250
|
const all: any = await queryRecords(made, {}, {}, run)
|
|
@@ -246,7 +268,12 @@ describe('amounts in more than one currency', () => {
|
|
|
246
268
|
...target,
|
|
247
269
|
key: 'office.fx-transaction',
|
|
248
270
|
query: target.query.replace('customerName account', 'customerName currency account'),
|
|
249
|
-
columns: [
|
|
271
|
+
columns: [
|
|
272
|
+
...target.columns.map(one =>
|
|
273
|
+
one.name === 'amount' ? { ...one, labelKey: 'field.amount', currency: 'currency' } : one
|
|
274
|
+
),
|
|
275
|
+
{ name: 'currency', type: 'string' }
|
|
276
|
+
]
|
|
250
277
|
}
|
|
251
278
|
const fxt = (i: number, amount: number, currency: string | null, customerName: string) => ({ ...tx(i, '2026-06-01', 'INCOME', amount, customerName), currency })
|
|
252
279
|
|
|
@@ -257,7 +284,7 @@ describe('amounts in more than one currency', () => {
|
|
|
257
284
|
expect(JSON.stringify(result.sums)).not.toMatch(/1100/)
|
|
258
285
|
})
|
|
259
286
|
|
|
260
|
-
it(
|
|
287
|
+
it("a line without its currency is summed as unknown, never as the domain's own", async () => {
|
|
261
288
|
ledger = [fxt(1, 1000, 'KRW', 'A'), fxt(2, 7, null, 'B')]
|
|
262
289
|
const result: any = await queryRecords(fx, {}, {}, run)
|
|
263
290
|
expect(result.sums.amount).toEqual({ KRW: 1000, unknown: 7 })
|
|
@@ -31,8 +31,8 @@ export interface ListRead {
|
|
|
31
31
|
search?: string
|
|
32
32
|
/** The screen's route. Left out, the list has no screen card. */
|
|
33
33
|
route?: string
|
|
34
|
-
/** The
|
|
35
|
-
|
|
34
|
+
/** The route has a URL-bound grid. Otherwise conditions are disclosed as not applied on its screen card. */
|
|
35
|
+
screenQuery?: boolean
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */
|
|
@@ -70,7 +70,9 @@ export function listReadTarget(list: ListRead): ReadTarget {
|
|
|
70
70
|
: [])
|
|
71
71
|
] as ReadCriterion[]
|
|
72
72
|
|
|
73
|
-
const takes =
|
|
73
|
+
const takes = list.screenQuery
|
|
74
|
+
? ['id', ...criteria.map(criterion => criterion.name), ...(search ? ['text'] : [])]
|
|
75
|
+
: []
|
|
74
76
|
return {
|
|
75
77
|
key: list.key,
|
|
76
78
|
labelKey: list.title,
|
|
@@ -90,12 +92,8 @@ export function listReadTarget(list: ListRead): ReadTarget {
|
|
|
90
92
|
route: list.route,
|
|
91
93
|
labelKey: list.title,
|
|
92
94
|
takes,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
...(takes.includes('month') && c.year ? { month: c.month } : {}),
|
|
96
|
-
...(takes.includes('flow') ? { flow: c.flow } : {}),
|
|
97
|
-
...(takes.includes('text') ? { search: c.text } : {})
|
|
98
|
-
})
|
|
95
|
+
queryState: !!list.screenQuery,
|
|
96
|
+
params: () => ({})
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
: {})
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
|
|
11
11
|
*/
|
|
12
12
|
import { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'
|
|
13
|
+
import { encodeListQuery } from '@operato/utils/list-query.js'
|
|
13
14
|
import type { GraphqlRunner } from '../data-entry/entry-update.js'
|
|
14
15
|
import { parse } from 'graphql'
|
|
15
16
|
|
|
@@ -41,6 +42,8 @@ export interface ReadColumn {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export interface ReadScreen {
|
|
45
|
+
/** A URL-bound grid accepts the same predicates used by readRows, including conditions outside its form. */
|
|
46
|
+
queryState?: boolean
|
|
44
47
|
/** The application's route of the list screen (`cash-transaction-list`). */
|
|
45
48
|
route: string
|
|
46
49
|
/** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */
|
|
@@ -109,7 +112,7 @@ export function declareReadTarget(target: ReadTarget): void {
|
|
|
109
112
|
if (!column.total || column.type !== 'number') throw new Error(`column '${column.name}' of '${target.key}' names a currency but is not a summed number`)
|
|
110
113
|
if (target.columns.find(one => one.name === column.currency)?.type !== 'string') throw new Error(`column '${column.name}' of '${target.key}' takes its currency from '${column.currency}', which is no string column`)
|
|
111
114
|
}
|
|
112
|
-
const names = new Set<string>()
|
|
115
|
+
const names = new Set<string>(['id'])
|
|
113
116
|
for (const criterion of target.criteria) {
|
|
114
117
|
if (criterion.name === 'id') throw new Error(`read target '${target.key}' declares 'id' - every target takes it`)
|
|
115
118
|
if (criterion.name === 'text' && target.search) throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`)
|
|
@@ -213,7 +216,9 @@ export function readCriteria(target: ReadTarget, args: any): { criteria: Record<
|
|
|
213
216
|
export function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[] {
|
|
214
217
|
return [
|
|
215
218
|
...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),
|
|
216
|
-
...target.criteria.flatMap(criterion =>
|
|
219
|
+
...target.criteria.flatMap(criterion =>
|
|
220
|
+
criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []
|
|
221
|
+
)
|
|
217
222
|
]
|
|
218
223
|
}
|
|
219
224
|
|
|
@@ -331,7 +336,14 @@ export function screenAddress(target: ReadTarget, criteria: Record<string, unkno
|
|
|
331
336
|
const params = new URLSearchParams()
|
|
332
337
|
for (const [name, value] of Object.entries(screen.params(criteria))) if (value !== undefined && value !== null && value !== '') params.set(name, String(value))
|
|
333
338
|
const given = Object.keys(criteria)
|
|
334
|
-
const
|
|
339
|
+
const applicable = Object.fromEntries(Object.entries(criteria).filter(([name]) => screen.takes.includes(name)))
|
|
340
|
+
const query = screen.queryState
|
|
341
|
+
? encodeListQuery({
|
|
342
|
+
filters: filtersOf(target, applicable),
|
|
343
|
+
search: typeof applicable.text === 'string' ? applicable.text : undefined,
|
|
344
|
+
sorters: target.sortings
|
|
345
|
+
})
|
|
346
|
+
: params.toString()
|
|
335
347
|
return {
|
|
336
348
|
href: query ? `${screen.route}?${query}` : screen.route,
|
|
337
349
|
applied: given.filter(name => screen.takes.includes(name)),
|