@things-factory/ai-assistant 10.1.113 → 10.1.116
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/client/components/assistant-chat.ts +19 -0
- package/client/components/assistant-dock.ts +26 -3
- package/client/setup-assistant-dock.ts +3 -2
- package/client/utils/entry-proposal.ts +14 -1
- package/client/utils/open-page.ts +21 -0
- package/dist-client/components/assistant-chat.d.ts +2 -0
- package/dist-client/components/assistant-chat.js +19 -0
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +5 -0
- package/dist-client/components/assistant-dock.js +25 -3
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/setup-assistant-dock.d.ts +1 -0
- package/dist-client/setup-assistant-dock.js +1 -0
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/entry-proposal.d.ts +10 -1
- package/dist-client/utils/entry-proposal.js +13 -1
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-client/utils/open-page.d.ts +5 -0
- package/dist-client/utils/open-page.js +19 -0
- package/dist-client/utils/open-page.js.map +1 -0
- package/dist-server/service/assistant-mode/resolve.d.ts +1 -0
- package/dist-server/service/assistant-mode/resolve.js +2 -1
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +2 -0
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tool-result.d.ts +28 -0
- package/dist-server/service/chat-session/attachment-tool-result.js +9 -0
- package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -0
- package/dist-server/service/chat-session/attachment-tools.js +4 -5
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +8 -0
- package/dist-server/service/data-entry/entry-apply.js +19 -2
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-refusal.d.ts +6 -0
- package/dist-server/service/data-entry/entry-refusal.js +25 -0
- package/dist-server/service/data-entry/entry-refusal.js.map +1 -0
- package/dist-server/service/data-entry/entry-resolver.js +2 -0
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
- package/dist-server/service/data-read/list-read.d.ts +36 -0
- package/dist-server/service/data-read/list-read.js +64 -0
- package/dist-server/service/data-read/list-read.js.map +1 -0
- package/dist-server/service/data-read/read-target.d.ts +109 -0
- package/dist-server/service/data-read/read-target.js +189 -0
- package/dist-server/service/data-read/read-target.js.map +1 -0
- package/dist-server/service/data-read/read-tools.d.ts +228 -0
- package/dist-server/service/data-read/read-tools.js +197 -0
- package/dist-server/service/data-read/read-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +3 -0
- package/dist-server/service/index.js +7 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/assistant-mode/resolve.ts +3 -2
- package/server/service/assistant-mode/types.ts +2 -0
- package/server/service/chat-session/attachment-tool-result.test.ts +26 -0
- package/server/service/chat-session/attachment-tool-result.ts +18 -0
- package/server/service/chat-session/attachment-tools.ts +4 -5
- package/server/service/data-entry/data-entry-update.test.ts +10 -1
- package/server/service/data-entry/data-entry.test.ts +29 -1
- package/server/service/data-entry/entry-apply.ts +27 -2
- package/server/service/data-entry/entry-refusal.test.ts +73 -0
- package/server/service/data-entry/entry-refusal.ts +20 -0
- package/server/service/data-entry/entry-resolver.ts +2 -0
- package/server/service/data-read/data-read.test.ts +229 -0
- package/server/service/data-read/list-read.ts +98 -0
- package/server/service/data-read/read-target.ts +230 -0
- package/server/service/data-read/read-tools.ts +212 -0
- package/server/service/index.ts +4 -0
- package/test/entry-proposal.test.ts +31 -1
- package/test/open-page.test.ts +31 -0
- package/translations/en.json +14 -0
- package/translations/ja.json +14 -0
- package/translations/ko.json +14 -0
- package/translations/ms.json +14 -0
- package/translations/zh.json +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-refusal.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-refusal.ts"],"names":[],"mappings":";;AAKA,0CAcC;AAnBD,yDAAiG;AACjG,mEAA0D;AAG1D,2FAA2F;AAC3F,SAAgB,eAAe,CAAC,OAA6C;IAC3E,OAAO,KAAK,EAAC,IAAI,EAAC,EAAE;QAClB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;QAC7E,MAAM,KAAK,GAAG,IAAA,0BAAS,EAAC,IAAI,CAAC,CAAA;QAC7B,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAA;QAChD,MAAM,KAAK,GAAG,IAAA,4BAAgB,EAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;QAC/D,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,OAAO,SAAS,CAAA;QACjD,MAAM,SAAS,GAAG,MAAM,IAAA,2BAAe,EAAC,KAAK,CAAC,IAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAA;QACxG,gGAAgG;QAChG,IAAI,SAAS,CAAC,SAAS,KAAK,KAAK;YAAE,OAAO,SAAS,CAAA;QACnD,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAA;QACpH,MAAM,KAAK,GAAG,IAAA,8BAAkB,EAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAClH,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAA;IAC1F,CAAC,CAAA;AACH,CAAC","sourcesContent":["import { judgePermission, privilegeOfField, privilegeRejection } from '@things-factory/auth-base'\nimport { doorField } from '../chat-message/door-access.js'\nimport type { EntryRefusal } from './entry-apply.js'\n\n/** Presentation only: AUTH still guards the application's mutation when it is executed. */\nexport function entryRefusalFor(context: { state?: any; t?: any } | undefined): (door: string) => Promise<EntryRefusal | undefined> {\n return async door => {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context?.state ?? {}\n const field = doorField(door)\n if (!domain || !user || !field) return undefined\n const asked = privilegeOfField(field.typeName, field.fieldName)\n if (!asked.known || !asked.gate) return undefined\n const judgement = await judgePermission(asked.gate as any, user, domain, unsafeIP, prohibitedPrivileges)\n /* The gate may have opened since the preflight: never invent a refusal to explain that race. */\n if (judgement.permitted !== false) return undefined\n const operation = field.typeName === 'Mutation' ? 'mutation' : field.typeName === 'Query' ? 'query' : 'subscription'\n const error = privilegeRejection(context, asked.gate.category, asked.gate.privilege, judgement.because, operation)\n return { message: error.message, extensions: error.extensions, path: [field.fieldName] }\n }\n}\n"]}
|
|
@@ -15,6 +15,7 @@ const proposal_outcome_js_1 = require("../chat-message/proposal-outcome.js");
|
|
|
15
15
|
const tool_access_js_1 = require("../tool-access.js");
|
|
16
16
|
const entry_apply_js_1 = require("./entry-apply.js");
|
|
17
17
|
const entry_tools_js_1 = require("./entry-tools.js");
|
|
18
|
+
const entry_refusal_js_1 = require("./entry-refusal.js");
|
|
18
19
|
let EntryProposalResolver = class EntryProposalResolver {
|
|
19
20
|
/*
|
|
20
21
|
* Not `@transaction`: each row is its own call of the application's mutation (entry-apply.ts), so one
|
|
@@ -46,6 +47,7 @@ let EntryProposalResolver = class EntryProposalResolver {
|
|
|
46
47
|
messages: carrying.map((message) => ({ id: message.id, proposals: message.proposalsJson ?? [] })),
|
|
47
48
|
answered: (0, entry_apply_js_1.entryProposalAnswered)(key, lines.map((line) => line.content)),
|
|
48
49
|
passesDoor: door => passes(door),
|
|
50
|
+
refusalFor: (0, entry_refusal_js_1.entryRefusalFor)(context),
|
|
49
51
|
state: context.state,
|
|
50
52
|
/*
|
|
51
53
|
* The application's API, as the person - the same request state the door would have seen had they typed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-resolver.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AACtE,qDAA6C;AAC7C,qCAA2C;AAE3C,6CAA4C;AAC5C,iDAAqD;AAErD,qEAA6D;AAC7D,qEAA6D;AAC7D,iFAA6E;AAC7E,6FAAwF;AACxF,6EAA6E;AAC7E,sDAAiD;AACjD,qDAAoG;AACpG,qDAA8C;
|
|
1
|
+
{"version":3,"file":"entry-resolver.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport { Like, Not, IsNull } from 'typeorm'\n\nimport { logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\nimport { ChatMessage } from '../chat-message/chat-message.js'\nimport { assertMayWriteSession } from '../chat-session/session-write-gate.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { PROPOSAL_OUTCOME_PREFIX } from '../chat-message/proposal-outcome.js'\nimport { toolAccessFor } from '../tool-access.js'\nimport { applyEntryProposal, entryProposalAnswered, type EntryApplyOutcome } from './entry-apply.js'\nimport { runAsCaller } from './entry-tools.js'\nimport { entryRefusalFor } from './entry-refusal.js'\n\n@Resolver()\nexport class EntryProposalResolver {\n /*\n * Not `@transaction`: each row is its own call of the application's mutation (entry-apply.ts), so one\n * refused row keeps the others. The dock reports the outcome afterwards through `reportProposalOutcome`.\n */\n @Mutation(() => GraphQLJSON, {\n description:\n 'Enter the rows of a data entry proposal the person pressed. The rows are the stored proposal\\'s; the door, a newer proposal and the target\\'s checks are asked again, and each row is written in its own transaction.'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\", domainOwnerGranted: true)')\n async applyEntryProposal(\n @Arg('sessionId') sessionId: string,\n @Arg('proposalKey') proposalKey: string,\n @Ctx() context: ResolverContext,\n @Arg('reviewedHash', { nullable: true, description: 'The fingerprint of the proposal as the AI workspace showed it; a proposal that is no longer that one is refused.' }) reviewedHash?: string\n ): Promise<EntryApplyOutcome> {\n const { domain, user } = context.state\n const session = await getRepository(ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } } as any)\n if (!session) throw new Error(`ChatSession ${sessionId} not found`)\n await assertMayWriteSession({\n session,\n userId: user?.id,\n isParticipant: () => isSessionParticipant(domain.id, session.id, user?.id),\n t: (key, options) => (context.t ? context.t(key, options) : String(options?.defaultValue ?? key))\n })\n\n const key = (proposalKey ?? '').trim()\n const carrying = await getRepository(ChatMessage).find({\n select: ['id', 'proposals', 'createdAt'] as any,\n where: { session: { id: session.id } as any, proposals: Not(IsNull()) } as any,\n order: { createdAt: 'ASC' } as any\n })\n const lines = await getRepository(ChatMessage).find({\n select: ['content'] as any,\n where: { session: { id: session.id } as any, role: 'system', content: Like(`${PROPOSAL_OUTCOME_PREFIX}${key}:%`) } as any\n })\n\n const passes = toolAccessFor(context)\n return await applyEntryProposal(key, {\n messages: carrying.map((message: any) => ({ id: message.id, proposals: message.proposalsJson ?? [] })),\n answered: entryProposalAnswered(key, lines.map((line: any) => line.content)),\n passesDoor: door => passes(door),\n refusalFor: entryRefusalFor(context),\n state: context.state,\n /*\n * The application's API, as the person - the same request state the door would have seen had they typed\n * the row in: domain, user, and what their address and prohibitions say. No `tx` is handed on, so the\n * door opens its own transaction per row (entry-tools.ts `runAsCaller`).\n */\n execute: runAsCaller(context),\n report: (message, error) => logger.error(message, error),\n reviewedHash: reviewedHash || undefined\n })\n }\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReadColumn, ReadTarget } from './read-target.js';
|
|
2
|
+
export interface ListRead {
|
|
3
|
+
/** `<app>.<record>`. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** i18n key of the screen's title - the card's name. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** The screen's name as the person knows it - the model picks the list by it. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** The ListParam query (`cashTransactions`). */
|
|
10
|
+
listQuery: string;
|
|
11
|
+
/** The fields of each item it reads - the screen's own selection where there is one. */
|
|
12
|
+
selection: string;
|
|
13
|
+
columns: ReadColumn[];
|
|
14
|
+
/** A date (or date-time) field the `from` / `to` conditions range over. */
|
|
15
|
+
dateField?: string;
|
|
16
|
+
/** Integer year and month fields, and an in/out field (INCOME | EXPENSE) - the `year`, `month`, `flow` conditions. */
|
|
17
|
+
narrowing?: {
|
|
18
|
+
year?: string;
|
|
19
|
+
month?: string;
|
|
20
|
+
flow?: string;
|
|
21
|
+
};
|
|
22
|
+
/** A field holding the period as YYYY-MM - the `period` condition. */
|
|
23
|
+
monthField?: string;
|
|
24
|
+
/** A status field and its values - the `status` condition. */
|
|
25
|
+
status?: {
|
|
26
|
+
field: string;
|
|
27
|
+
values: string[];
|
|
28
|
+
};
|
|
29
|
+
/** The resolver's searchables, said for the model - the list takes ListParam search (the `text` condition). */
|
|
30
|
+
search?: string;
|
|
31
|
+
/** The screen's route. Left out, the list has no screen card. */
|
|
32
|
+
route?: string;
|
|
33
|
+
/** The conditions the screen reads from its address - `year`, `month`, `flow` and `text` (as `search`). Empty: it opens as it is. */
|
|
34
|
+
screenTakes?: ('year' | 'month' | 'flow' | 'text')[];
|
|
35
|
+
}
|
|
36
|
+
export declare function listReadTarget(list: ListRead): ReadTarget;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listReadTarget = listReadTarget;
|
|
4
|
+
/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */
|
|
5
|
+
const amountOf = (list) => list.columns.find(one => one.name === 'amount' && one.type === 'number')?.name ?? list.columns.find(one => one.total)?.name;
|
|
6
|
+
function listReadTarget(list) {
|
|
7
|
+
const search = !!list.search;
|
|
8
|
+
const amount = amountOf(list);
|
|
9
|
+
const narrowing = list.narrowing ?? {};
|
|
10
|
+
const criteria = [
|
|
11
|
+
...(list.dateField
|
|
12
|
+
? [
|
|
13
|
+
{ name: 'from', type: 'date', labelKey: 'ai-assistant.read.from', description: `the first ${list.dateField}, inclusive`, filter: (v) => ({ name: list.dateField, operator: 'gte', value: v }) },
|
|
14
|
+
{ name: 'to', type: 'date', labelKey: 'ai-assistant.read.to', description: `the last ${list.dateField}, inclusive`, filter: (v) => ({ name: list.dateField, operator: 'lte', value: `${v}${/T/.test(String(v)) ? '' : 'T23:59:59.999'}` }) }
|
|
15
|
+
]
|
|
16
|
+
: []),
|
|
17
|
+
...(narrowing.year ? [{ name: 'year', type: 'integer', labelKey: 'ai-assistant.read.year', description: 'a year (2026)', filter: (v) => ({ name: narrowing.year, operator: 'eq', value: v }) }] : []),
|
|
18
|
+
...(narrowing.month ? [{ name: 'month', type: 'integer', labelKey: 'ai-assistant.read.month', description: '1-12, with a year', filter: (v) => ({ name: narrowing.month, operator: 'eq', value: v }) }] : []),
|
|
19
|
+
...(narrowing.flow
|
|
20
|
+
? [{ name: 'flow', type: 'enum', values: ['INCOME', 'EXPENSE'], labelKey: 'ai-assistant.read.flow', description: 'money in or out', filter: (v) => ({ name: narrowing.flow, operator: 'eq', value: v }) }]
|
|
21
|
+
: []),
|
|
22
|
+
...(list.monthField
|
|
23
|
+
? [{ name: 'period', type: 'string', labelKey: 'ai-assistant.read.period', description: 'a month as YYYY-MM', filter: (v) => ({ name: list.monthField, operator: 'eq', value: v }) }]
|
|
24
|
+
: []),
|
|
25
|
+
...(list.status
|
|
26
|
+
? [{ name: 'status', type: 'enum', values: list.status.values, labelKey: 'ai-assistant.read.status', filter: (v) => ({ name: list.status.field, operator: 'eq', value: v }) }]
|
|
27
|
+
: []),
|
|
28
|
+
...(amount
|
|
29
|
+
? [
|
|
30
|
+
{ name: 'minAmount', type: 'number', labelKey: 'ai-assistant.read.min-amount', description: `at least this ${amount}`, filter: (v) => ({ name: amount, operator: 'gte', value: v }) },
|
|
31
|
+
{ name: 'maxAmount', type: 'number', labelKey: 'ai-assistant.read.max-amount', description: `at most this ${amount}`, filter: (v) => ({ name: amount, operator: 'lte', value: v }) }
|
|
32
|
+
]
|
|
33
|
+
: [])
|
|
34
|
+
];
|
|
35
|
+
const takes = (list.screenTakes ?? []).filter(name => (name === 'text' ? search : !!narrowing[name]));
|
|
36
|
+
return {
|
|
37
|
+
key: list.key,
|
|
38
|
+
labelKey: list.title,
|
|
39
|
+
description: `the rows of the list 「${list.name}」 (${list.columns.map(one => one.name).join(', ')}).`,
|
|
40
|
+
query: `query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]${search ? ', $search: String' : ''}) ` +
|
|
41
|
+
`{ ${list.listQuery}(filters: $filters, pagination: $pagination, sortings: $sortings${search ? ', search: $search' : ''}) { items { ${list.selection} } total } }`,
|
|
42
|
+
criteria,
|
|
43
|
+
columns: list.columns,
|
|
44
|
+
rowsOf: data => ({ rows: data?.[list.listQuery]?.items ?? [], total: data?.[list.listQuery]?.total ?? 0 }),
|
|
45
|
+
...(search ? { search: { description: `the ${list.search}` } } : {}),
|
|
46
|
+
...(list.dateField ? { sortings: [{ name: list.dateField, desc: true }] } : {}),
|
|
47
|
+
...(list.route
|
|
48
|
+
? {
|
|
49
|
+
screen: {
|
|
50
|
+
route: list.route,
|
|
51
|
+
labelKey: list.title,
|
|
52
|
+
takes,
|
|
53
|
+
params: (c) => ({
|
|
54
|
+
...(takes.includes('year') ? { year: c.year } : {}),
|
|
55
|
+
...(takes.includes('month') && c.year ? { month: c.month } : {}),
|
|
56
|
+
...(takes.includes('flow') ? { flow: c.flow } : {}),
|
|
57
|
+
...(takes.includes('text') ? { search: c.text } : {})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
: {})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=list-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-read.js","sourceRoot":"","sources":["../../../server/service/data-read/list-read.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A list declared in one line (operato-application/design/ai/read-targets.md §3) - the shape most list screens share:\n * a ListParam query, the fields it reads, a date to range over, a status, an amount, search words, and a screen.\n * Each application writes its lists as these lines; this turns one into a read target with the conditions it takes.\n */\nimport type { ReadColumn, ReadCriterion, ReadTarget } from './read-target.js'\n\nexport interface ListRead {\n /** `<app>.<record>`. */\n key: string\n /** i18n key of the screen's title - the card's name. */\n title: string\n /** The screen's name as the person knows it - the model picks the list by it. */\n name: string\n /** The ListParam query (`cashTransactions`). */\n listQuery: string\n /** The fields of each item it reads - the screen's own selection where there is one. */\n selection: string\n columns: ReadColumn[]\n /** A date (or date-time) field the `from` / `to` conditions range over. */\n dateField?: string\n /** Integer year and month fields, and an in/out field (INCOME | EXPENSE) - the `year`, `month`, `flow` conditions. */\n narrowing?: { year?: string; month?: string; flow?: string }\n /** A field holding the period as YYYY-MM - the `period` condition. */\n monthField?: string\n /** A status field and its values - the `status` condition. */\n status?: { field: string; values: string[] }\n /** The resolver's searchables, said for the model - the list takes ListParam search (the `text` condition). */\n search?: string\n /** The screen's route. Left out, the list has no screen card. */\n route?: string\n /** The conditions the screen reads from its address - `year`, `month`, `flow` and `text` (as `search`). Empty: it opens as it is. */\n screenTakes?: ('year' | 'month' | 'flow' | 'text')[]\n}\n\n/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */\nconst amountOf = (list: ListRead) => list.columns.find(one => one.name === 'amount' && one.type === 'number')?.name ?? list.columns.find(one => one.total)?.name\n\nexport function listReadTarget(list: ListRead): ReadTarget {\n const search = !!list.search\n const amount = amountOf(list)\n const narrowing = list.narrowing ?? {}\n const criteria: ReadCriterion[] = [\n ...(list.dateField\n ? [\n { name: 'from', type: 'date', labelKey: 'ai-assistant.read.from', description: `the first ${list.dateField}, inclusive`, filter: (v: any) => ({ name: list.dateField!, operator: 'gte', value: v }) },\n { name: 'to', type: 'date', labelKey: 'ai-assistant.read.to', description: `the last ${list.dateField}, inclusive`, filter: (v: any) => ({ name: list.dateField!, operator: 'lte', value: `${v}${/T/.test(String(v)) ? '' : 'T23:59:59.999'}` }) }\n ]\n : []),\n ...(narrowing.year ? [{ name: 'year', type: 'integer', labelKey: 'ai-assistant.read.year', description: 'a year (2026)', filter: (v: any) => ({ name: narrowing.year!, operator: 'eq', value: v }) }] : []),\n ...(narrowing.month ? [{ name: 'month', type: 'integer', labelKey: 'ai-assistant.read.month', description: '1-12, with a year', filter: (v: any) => ({ name: narrowing.month!, operator: 'eq', value: v }) }] : []),\n ...(narrowing.flow\n ? [{ name: 'flow', type: 'enum', values: ['INCOME', 'EXPENSE'], labelKey: 'ai-assistant.read.flow', description: 'money in or out', filter: (v: any) => ({ name: narrowing.flow!, operator: 'eq', value: v }) }]\n : []),\n ...(list.monthField\n ? [{ name: 'period', type: 'string', labelKey: 'ai-assistant.read.period', description: 'a month as YYYY-MM', filter: (v: any) => ({ name: list.monthField!, operator: 'eq', value: v }) }]\n : []),\n ...(list.status\n ? [{ name: 'status', type: 'enum', values: list.status.values, labelKey: 'ai-assistant.read.status', filter: (v: any) => ({ name: list.status!.field, operator: 'eq', value: v }) }]\n : []),\n ...(amount\n ? [\n { name: 'minAmount', type: 'number', labelKey: 'ai-assistant.read.min-amount', description: `at least this ${amount}`, filter: (v: any) => ({ name: amount, operator: 'gte', value: v }) },\n { name: 'maxAmount', type: 'number', labelKey: 'ai-assistant.read.max-amount', description: `at most this ${amount}`, filter: (v: any) => ({ name: amount, operator: 'lte', value: v }) }\n ]\n : [])\n ] as ReadCriterion[]\n\n const takes = (list.screenTakes ?? []).filter(name => (name === 'text' ? search : !!narrowing[name]))\n return {\n key: list.key,\n labelKey: list.title,\n description: `the rows of the list 「${list.name}」 (${list.columns.map(one => one.name).join(', ')}).`,\n query:\n `query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]${search ? ', $search: String' : ''}) ` +\n `{ ${list.listQuery}(filters: $filters, pagination: $pagination, sortings: $sortings${search ? ', search: $search' : ''}) { items { ${list.selection} } total } }`,\n criteria,\n columns: list.columns,\n rowsOf: data => ({ rows: data?.[list.listQuery]?.items ?? [], total: data?.[list.listQuery]?.total ?? 0 }),\n ...(search ? { search: { description: `the ${list.search}` } } : {}),\n ...(list.dateField ? { sortings: [{ name: list.dateField, desc: true }] } : {}),\n ...(list.route\n ? {\n screen: {\n route: list.route,\n labelKey: list.title,\n takes,\n params: (c: Record<string, any>) => ({\n ...(takes.includes('year') ? { year: c.year } : {}),\n ...(takes.includes('month') && c.year ? { month: c.month } : {}),\n ...(takes.includes('flow') ? { flow: c.flow } : {}),\n ...(takes.includes('text') ? { search: c.text } : {})\n })\n }\n }\n : {})\n }\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type EntryField } from '../data-entry/entry-target.js';
|
|
2
|
+
import type { GraphqlRunner } from '../data-entry/entry-update.js';
|
|
3
|
+
export interface ReadFilter {
|
|
4
|
+
name: string;
|
|
5
|
+
operator: string;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** A condition the model may set - read with the entry fields' rules, turned into the query's filters by the application. */
|
|
9
|
+
export interface ReadCriterion extends Omit<EntryField, 'required' | 'reference' | 'total'> {
|
|
10
|
+
filter: (value: any) => ReadFilter | ReadFilter[];
|
|
11
|
+
}
|
|
12
|
+
export interface ReadColumn {
|
|
13
|
+
/** The key in the workspace table. */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Where the value is in a row - a dotted path into the query's item (`account.name`). Defaults to `name`. */
|
|
16
|
+
path?: string;
|
|
17
|
+
type: 'string' | 'number' | 'date';
|
|
18
|
+
labelKey?: string;
|
|
19
|
+
/** A number column the server sums. */
|
|
20
|
+
total?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ReadScreen {
|
|
23
|
+
/** The application's route of the list screen (`cash-transaction-list`). */
|
|
24
|
+
route: string;
|
|
25
|
+
/** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */
|
|
26
|
+
params: (criteria: Record<string, any>) => Record<string, string | number | undefined>;
|
|
27
|
+
/** The conditions `params` carries - the card names the others as not applied on the screen. */
|
|
28
|
+
takes: string[];
|
|
29
|
+
labelKey?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ReadTarget {
|
|
32
|
+
/** `<app>.<record>` - the tools are named after it. */
|
|
33
|
+
key: string;
|
|
34
|
+
/** i18n key naming the records (「은행 입출금」). */
|
|
35
|
+
labelKey?: string;
|
|
36
|
+
/** What the records are, for the model. */
|
|
37
|
+
description: string;
|
|
38
|
+
/**
|
|
39
|
+
* The list query - takes `$filters: [Filter!]`, `$pagination: Pagination` and `$sortings: [Sorting!]`, and
|
|
40
|
+
* `$search: String` when `search` is declared.
|
|
41
|
+
*/
|
|
42
|
+
query: string;
|
|
43
|
+
/**
|
|
44
|
+
* The list takes ListParam's free-text term, matched by the resolver across its own searchable fields - the
|
|
45
|
+
* condition `text`. Say what it finds in, for the model.
|
|
46
|
+
*/
|
|
47
|
+
search?: {
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
criteria: ReadCriterion[];
|
|
51
|
+
columns: ReadColumn[];
|
|
52
|
+
rowsOf: (data: any) => {
|
|
53
|
+
rows: any[];
|
|
54
|
+
total: number;
|
|
55
|
+
};
|
|
56
|
+
/** Orders the rows; the query's own order when left out. */
|
|
57
|
+
sortings?: {
|
|
58
|
+
name: string;
|
|
59
|
+
desc?: boolean;
|
|
60
|
+
}[];
|
|
61
|
+
screen?: ReadScreen;
|
|
62
|
+
}
|
|
63
|
+
/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */
|
|
64
|
+
export declare const MAX_READ_ROWS = 1000;
|
|
65
|
+
/** Rows the model is shown; the rest stay on the card. */
|
|
66
|
+
export declare const ROWS_FOR_MODEL = 20;
|
|
67
|
+
/** A page of the list query. */
|
|
68
|
+
export declare const READ_PAGE = 200;
|
|
69
|
+
export declare const queryToolName: (key: string) => string;
|
|
70
|
+
export declare const screenToolName: (key: string) => string;
|
|
71
|
+
export declare const readCategoryName: (key: string) => string;
|
|
72
|
+
export declare function declareReadTarget(target: ReadTarget): void;
|
|
73
|
+
export declare function readTargetOf(key: string | undefined): ReadTarget | undefined;
|
|
74
|
+
export declare function clearReadTargets(): void;
|
|
75
|
+
/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */
|
|
76
|
+
export declare function readCriteria(target: ReadTarget, args: any): {
|
|
77
|
+
criteria: Record<string, unknown>;
|
|
78
|
+
refused: {
|
|
79
|
+
name: string;
|
|
80
|
+
code: string;
|
|
81
|
+
}[];
|
|
82
|
+
};
|
|
83
|
+
export declare function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[];
|
|
84
|
+
/** One row as the columns say - a number column as a number, a missing value as null. */
|
|
85
|
+
export declare function rowOf(target: ReadTarget, item: any): Record<string, string | number | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the
|
|
88
|
+
* list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readRows(target: ReadTarget, filters: ReadFilter[], run: GraphqlRunner, search?: string): Promise<{
|
|
91
|
+
rows: Record<string, any>[];
|
|
92
|
+
total: number;
|
|
93
|
+
complete: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
export interface ReadGroup {
|
|
96
|
+
key: string;
|
|
97
|
+
count: number;
|
|
98
|
+
sums: Record<string, number>;
|
|
99
|
+
}
|
|
100
|
+
/** The sums of the number columns marked total - the server's, never the model's. */
|
|
101
|
+
export declare function sumsOf(target: ReadTarget, rows: Record<string, any>[]): Record<string, number>;
|
|
102
|
+
/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */
|
|
103
|
+
export declare function groupsOf(target: ReadTarget, rows: Record<string, any>[], by: string): ReadGroup[] | undefined;
|
|
104
|
+
/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */
|
|
105
|
+
export declare function screenAddress(target: ReadTarget, criteria: Record<string, unknown>): {
|
|
106
|
+
href: string;
|
|
107
|
+
applied: string[];
|
|
108
|
+
notApplied: string[];
|
|
109
|
+
} | undefined;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readCategoryName = exports.screenToolName = exports.queryToolName = exports.READ_PAGE = exports.ROWS_FOR_MODEL = exports.MAX_READ_ROWS = void 0;
|
|
4
|
+
exports.declareReadTarget = declareReadTarget;
|
|
5
|
+
exports.readTargetOf = readTargetOf;
|
|
6
|
+
exports.clearReadTargets = clearReadTargets;
|
|
7
|
+
exports.readCriteria = readCriteria;
|
|
8
|
+
exports.filtersOf = filtersOf;
|
|
9
|
+
exports.rowOf = rowOf;
|
|
10
|
+
exports.readRows = readRows;
|
|
11
|
+
exports.sumsOf = sumsOf;
|
|
12
|
+
exports.groupsOf = groupsOf;
|
|
13
|
+
exports.screenAddress = screenAddress;
|
|
14
|
+
/*
|
|
15
|
+
* AI 조회 - an application's list, read through its own query as the person (operato-application/design/ai/read-targets.md).
|
|
16
|
+
*
|
|
17
|
+
* The application declares what it lets the assistant read: its list query, the conditions the model may set and
|
|
18
|
+
* how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.
|
|
19
|
+
* The framework reads the rows through that query with the person's request state - the same @privilege and
|
|
20
|
+
* domain as the screen - counts and sums them itself, and puts a long result on the AI 작업판 as the query returned
|
|
21
|
+
* it. The model is told the count, the sums and a few rows; it never copies a table.
|
|
22
|
+
*
|
|
23
|
+
* Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
|
|
24
|
+
*/
|
|
25
|
+
const entry_target_js_1 = require("../data-entry/entry-target.js");
|
|
26
|
+
/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */
|
|
27
|
+
exports.MAX_READ_ROWS = 1000;
|
|
28
|
+
/** Rows the model is shown; the rest stay on the card. */
|
|
29
|
+
exports.ROWS_FOR_MODEL = 20;
|
|
30
|
+
/** A page of the list query. */
|
|
31
|
+
exports.READ_PAGE = 200;
|
|
32
|
+
const registry = new Map();
|
|
33
|
+
const pascalOf = (key) => key
|
|
34
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
37
|
+
.join('');
|
|
38
|
+
const queryToolName = (key) => `query${pascalOf(key)}`;
|
|
39
|
+
exports.queryToolName = queryToolName;
|
|
40
|
+
const screenToolName = (key) => `open${pascalOf(key)}Screen`;
|
|
41
|
+
exports.screenToolName = screenToolName;
|
|
42
|
+
const readCategoryName = (key) => `data-read:${key}`;
|
|
43
|
+
exports.readCategoryName = readCategoryName;
|
|
44
|
+
function declareReadTarget(target) {
|
|
45
|
+
if (!/^[a-z0-9-]+\.[a-z0-9-]+$/.test(target.key))
|
|
46
|
+
throw new Error(`read target key '${target.key}' is not <app>.<record>`);
|
|
47
|
+
if (registry.has(target.key))
|
|
48
|
+
throw new Error(`read target '${target.key}' is declared twice`);
|
|
49
|
+
(0, entry_target_js_1.rootFieldOf)(target.query, 'query');
|
|
50
|
+
if (!target.columns.length)
|
|
51
|
+
throw new Error(`read target '${target.key}' shows no column`);
|
|
52
|
+
const names = new Set();
|
|
53
|
+
for (const criterion of target.criteria) {
|
|
54
|
+
if (criterion.name === 'id')
|
|
55
|
+
throw new Error(`read target '${target.key}' declares 'id' - every target takes it`);
|
|
56
|
+
if (criterion.name === 'text' && target.search)
|
|
57
|
+
throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`);
|
|
58
|
+
if (names.has(criterion.name))
|
|
59
|
+
throw new Error(`read target '${target.key}' names the condition '${criterion.name}' twice`);
|
|
60
|
+
names.add(criterion.name);
|
|
61
|
+
if (criterion.type === 'enum' && !criterion.values?.length)
|
|
62
|
+
throw new Error(`condition '${criterion.name}' of '${target.key}' lists no values`);
|
|
63
|
+
}
|
|
64
|
+
if (target.search)
|
|
65
|
+
names.add('text');
|
|
66
|
+
for (const taken of target.screen?.takes ?? [])
|
|
67
|
+
if (!names.has(taken))
|
|
68
|
+
throw new Error(`the screen of '${target.key}' takes '${taken}', which is no condition`);
|
|
69
|
+
registry.set(target.key, target);
|
|
70
|
+
}
|
|
71
|
+
function readTargetOf(key) {
|
|
72
|
+
return key ? registry.get(key) : undefined;
|
|
73
|
+
}
|
|
74
|
+
function clearReadTargets() {
|
|
75
|
+
registry.clear();
|
|
76
|
+
}
|
|
77
|
+
/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */
|
|
78
|
+
function readCriteria(target, args) {
|
|
79
|
+
const criteria = {};
|
|
80
|
+
const refused = [];
|
|
81
|
+
/* One record by its id - every target takes it (the id a list answer or a find tool gave). */
|
|
82
|
+
if (typeof args?.id === 'string' && args.id.trim())
|
|
83
|
+
criteria.id = args.id.trim();
|
|
84
|
+
if (target.search && typeof args?.text === 'string' && args.text.trim())
|
|
85
|
+
criteria.text = args.text.trim();
|
|
86
|
+
for (const criterion of target.criteria) {
|
|
87
|
+
const value = args?.[criterion.name];
|
|
88
|
+
if (value === undefined || value === null || value === '')
|
|
89
|
+
continue;
|
|
90
|
+
const { read, code } = (0, entry_target_js_1.readFieldValue)(criterion, value);
|
|
91
|
+
if (read === undefined)
|
|
92
|
+
refused.push({ name: criterion.name, code: code ?? 'unreadable' });
|
|
93
|
+
else
|
|
94
|
+
criteria[criterion.name] = read;
|
|
95
|
+
}
|
|
96
|
+
return { criteria, refused };
|
|
97
|
+
}
|
|
98
|
+
function filtersOf(target, criteria) {
|
|
99
|
+
return [
|
|
100
|
+
...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),
|
|
101
|
+
...target.criteria.flatMap(criterion => (criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []))
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
const valueAt = (row, path) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row);
|
|
105
|
+
/** One row as the columns say - a number column as a number, a missing value as null. */
|
|
106
|
+
function rowOf(target, item) {
|
|
107
|
+
return Object.fromEntries([
|
|
108
|
+
/* The id names the record to the model - for one record's detail, a change, or its screen. */
|
|
109
|
+
...(item?.id ? [['id', String(item.id)]] : []),
|
|
110
|
+
...target.columns.map(column => {
|
|
111
|
+
const raw = valueAt(item, column.path ?? column.name);
|
|
112
|
+
if (raw === undefined || raw === null || raw === '')
|
|
113
|
+
return [column.name, null];
|
|
114
|
+
if (column.type === 'number') {
|
|
115
|
+
const number = Number(raw);
|
|
116
|
+
return [column.name, Number.isFinite(number) ? number : null];
|
|
117
|
+
}
|
|
118
|
+
return [column.name, String(raw)];
|
|
119
|
+
})
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the
|
|
124
|
+
* list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.
|
|
125
|
+
*/
|
|
126
|
+
async function readRows(target, filters, run, search) {
|
|
127
|
+
const rows = [];
|
|
128
|
+
let total = 0;
|
|
129
|
+
for (let page = 1; rows.length < exports.MAX_READ_ROWS; page++) {
|
|
130
|
+
const answer = await run(target.query, {
|
|
131
|
+
filters,
|
|
132
|
+
...(search ? { search } : {}),
|
|
133
|
+
pagination: { page, limit: exports.READ_PAGE },
|
|
134
|
+
...(target.sortings ? { sortings: target.sortings } : {})
|
|
135
|
+
});
|
|
136
|
+
if (answer.errors?.length)
|
|
137
|
+
throw new Error(answer.errors.map(one => one.message).join('; '));
|
|
138
|
+
const read = target.rowsOf(answer.data);
|
|
139
|
+
total = Number(read?.total) || 0;
|
|
140
|
+
const items = read?.rows ?? [];
|
|
141
|
+
rows.push(...items.slice(0, exports.MAX_READ_ROWS - rows.length).map(item => rowOf(target, item)));
|
|
142
|
+
if (items.length < exports.READ_PAGE || rows.length >= total)
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
return { rows, total, complete: rows.length >= total };
|
|
146
|
+
}
|
|
147
|
+
/** The sums of the number columns marked total - the server's, never the model's. */
|
|
148
|
+
function sumsOf(target, rows) {
|
|
149
|
+
const sums = {};
|
|
150
|
+
for (const column of target.columns.filter(one => one.total && one.type === 'number')) {
|
|
151
|
+
sums[column.name] = Math.round(rows.reduce((sum, row) => sum + (typeof row[column.name] === 'number' ? row[column.name] : 0), 0) * 100) / 100;
|
|
152
|
+
}
|
|
153
|
+
return sums;
|
|
154
|
+
}
|
|
155
|
+
/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */
|
|
156
|
+
function groupsOf(target, rows, by) {
|
|
157
|
+
const column = target.columns.find(one => one.name === by);
|
|
158
|
+
if (!column)
|
|
159
|
+
return undefined;
|
|
160
|
+
const groups = new Map();
|
|
161
|
+
for (const row of rows) {
|
|
162
|
+
const value = row[by];
|
|
163
|
+
const key = value === null || value === undefined ? '' : column.type === 'date' ? String(value).slice(0, 7) : String(value);
|
|
164
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
165
|
+
}
|
|
166
|
+
const list = [...groups].map(([key, members]) => ({ key, count: members.length, sums: sumsOf(target, members) }));
|
|
167
|
+
const first = target.columns.find(one => one.total)?.name;
|
|
168
|
+
return column.type === 'date'
|
|
169
|
+
? list.sort((a, b) => a.key.localeCompare(b.key))
|
|
170
|
+
: list.sort((a, b) => (first ? Math.abs(b.sums[first] ?? 0) - Math.abs(a.sums[first] ?? 0) : b.count - a.count));
|
|
171
|
+
}
|
|
172
|
+
/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */
|
|
173
|
+
function screenAddress(target, criteria) {
|
|
174
|
+
const screen = target.screen;
|
|
175
|
+
if (!screen)
|
|
176
|
+
return undefined;
|
|
177
|
+
const params = new URLSearchParams();
|
|
178
|
+
for (const [name, value] of Object.entries(screen.params(criteria)))
|
|
179
|
+
if (value !== undefined && value !== null && value !== '')
|
|
180
|
+
params.set(name, String(value));
|
|
181
|
+
const given = Object.keys(criteria);
|
|
182
|
+
const query = params.toString();
|
|
183
|
+
return {
|
|
184
|
+
href: query ? `${screen.route}?${query}` : screen.route,
|
|
185
|
+
applied: given.filter(name => screen.takes.includes(name)),
|
|
186
|
+
notApplied: given.filter(name => !screen.takes.includes(name))
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=read-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-target.js","sourceRoot":"","sources":["../../../server/service/data-read/read-target.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * AI 조회 - an application's list, read through its own query as the person (operato-application/design/ai/read-targets.md).\n *\n * The application declares what it lets the assistant read: its list query, the conditions the model may set and\n * how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.\n * The framework reads the rows through that query with the person's request state - the same @privilege and\n * domain as the screen - counts and sums them itself, and puts a long result on the AI 작업판 as the query returned\n * it. The model is told the count, the sums and a few rows; it never copies a table.\n *\n * Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.\n */\nimport { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'\nimport type { GraphqlRunner } from '../data-entry/entry-update.js'\n\nexport interface ReadFilter {\n name: string\n operator: string\n value: unknown\n}\n\n/** A condition the model may set - read with the entry fields' rules, turned into the query's filters by the application. */\nexport interface ReadCriterion extends Omit<EntryField, 'required' | 'reference' | 'total'> {\n filter: (value: any) => ReadFilter | ReadFilter[]\n}\n\nexport interface ReadColumn {\n /** The key in the workspace table. */\n name: string\n /** Where the value is in a row - a dotted path into the query's item (`account.name`). Defaults to `name`. */\n path?: string\n type: 'string' | 'number' | 'date'\n labelKey?: string\n /** A number column the server sums. */\n total?: boolean\n}\n\nexport interface ReadScreen {\n /** The application's route of the list screen (`cash-transaction-list`). */\n route: string\n /** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */\n params: (criteria: Record<string, any>) => Record<string, string | number | undefined>\n /** The conditions `params` carries - the card names the others as not applied on the screen. */\n takes: string[]\n labelKey?: string\n}\n\nexport interface ReadTarget {\n /** `<app>.<record>` - the tools are named after it. */\n key: string\n /** i18n key naming the records (「은행 입출금」). */\n labelKey?: string\n /** What the records are, for the model. */\n description: string\n /**\n * The list query - takes `$filters: [Filter!]`, `$pagination: Pagination` and `$sortings: [Sorting!]`, and\n * `$search: String` when `search` is declared.\n */\n query: string\n /**\n * The list takes ListParam's free-text term, matched by the resolver across its own searchable fields - the\n * condition `text`. Say what it finds in, for the model.\n */\n search?: { description: string }\n criteria: ReadCriterion[]\n columns: ReadColumn[]\n rowsOf: (data: any) => { rows: any[]; total: number }\n /** Orders the rows; the query's own order when left out. */\n sortings?: { name: string; desc?: boolean }[]\n screen?: ReadScreen\n}\n\n/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */\nexport const MAX_READ_ROWS = 1000\n/** Rows the model is shown; the rest stay on the card. */\nexport const ROWS_FOR_MODEL = 20\n/** A page of the list query. */\nexport const READ_PAGE = 200\n\nconst registry = new Map<string, ReadTarget>()\n\nconst pascalOf = (key: string) =>\n key\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map(part => part[0].toUpperCase() + part.slice(1))\n .join('')\n\nexport const queryToolName = (key: string) => `query${pascalOf(key)}`\nexport const screenToolName = (key: string) => `open${pascalOf(key)}Screen`\nexport const readCategoryName = (key: string) => `data-read:${key}`\n\nexport function declareReadTarget(target: ReadTarget): void {\n if (!/^[a-z0-9-]+\\.[a-z0-9-]+$/.test(target.key)) throw new Error(`read target key '${target.key}' is not <app>.<record>`)\n if (registry.has(target.key)) throw new Error(`read target '${target.key}' is declared twice`)\n rootFieldOf(target.query, 'query')\n if (!target.columns.length) throw new Error(`read target '${target.key}' shows no column`)\n const names = new Set<string>()\n for (const criterion of target.criteria) {\n if (criterion.name === 'id') throw new Error(`read target '${target.key}' declares 'id' - every target takes it`)\n if (criterion.name === 'text' && target.search) throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`)\n if (names.has(criterion.name)) throw new Error(`read target '${target.key}' names the condition '${criterion.name}' twice`)\n names.add(criterion.name)\n if (criterion.type === 'enum' && !criterion.values?.length) throw new Error(`condition '${criterion.name}' of '${target.key}' lists no values`)\n }\n if (target.search) names.add('text')\n for (const taken of target.screen?.takes ?? []) if (!names.has(taken)) throw new Error(`the screen of '${target.key}' takes '${taken}', which is no condition`)\n registry.set(target.key, target)\n}\n\nexport function readTargetOf(key: string | undefined): ReadTarget | undefined {\n return key ? registry.get(key) : undefined\n}\n\nexport function clearReadTargets(): void {\n registry.clear()\n}\n\n/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */\nexport function readCriteria(target: ReadTarget, args: any): { criteria: Record<string, unknown>; refused: { name: string; code: string }[] } {\n const criteria: Record<string, unknown> = {}\n const refused: { name: string; code: string }[] = []\n /* One record by its id - every target takes it (the id a list answer or a find tool gave). */\n if (typeof args?.id === 'string' && args.id.trim()) criteria.id = args.id.trim()\n if (target.search && typeof args?.text === 'string' && args.text.trim()) criteria.text = args.text.trim()\n for (const criterion of target.criteria) {\n const value = args?.[criterion.name]\n if (value === undefined || value === null || value === '') continue\n const { read, code } = readFieldValue(criterion as EntryField, value)\n if (read === undefined) refused.push({ name: criterion.name, code: code ?? 'unreadable' })\n else criteria[criterion.name] = read\n }\n return { criteria, refused }\n}\n\nexport function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[] {\n return [\n ...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),\n ...target.criteria.flatMap(criterion => (criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []))\n ]\n}\n\nconst valueAt = (row: any, path: string) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row)\n\n/** One row as the columns say - a number column as a number, a missing value as null. */\nexport function rowOf(target: ReadTarget, item: any): Record<string, string | number | null> {\n return Object.fromEntries([\n /* The id names the record to the model - for one record's detail, a change, or its screen. */\n ...(item?.id ? [['id', String(item.id)] as [string, string]] : []),\n ...target.columns.map(column => {\n const raw = valueAt(item, column.path ?? column.name)\n if (raw === undefined || raw === null || raw === '') return [column.name, null]\n if (column.type === 'number') {\n const number = Number(raw)\n return [column.name, Number.isFinite(number) ? number : null]\n }\n return [column.name, String(raw)]\n })\n ])\n}\n\n/**\n * Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the\n * list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.\n */\nexport async function readRows(target: ReadTarget, filters: ReadFilter[], run: GraphqlRunner, search?: string): Promise<{ rows: Record<string, any>[]; total: number; complete: boolean }> {\n const rows: Record<string, any>[] = []\n let total = 0\n for (let page = 1; rows.length < MAX_READ_ROWS; page++) {\n const answer = await run(target.query, {\n filters,\n ...(search ? { search } : {}),\n pagination: { page, limit: READ_PAGE },\n ...(target.sortings ? { sortings: target.sortings } : {})\n })\n if (answer.errors?.length) throw new Error(answer.errors.map(one => one.message).join('; '))\n const read = target.rowsOf(answer.data)\n total = Number(read?.total) || 0\n const items = read?.rows ?? []\n rows.push(...items.slice(0, MAX_READ_ROWS - rows.length).map(item => rowOf(target, item)))\n if (items.length < READ_PAGE || rows.length >= total) break\n }\n return { rows, total, complete: rows.length >= total }\n}\n\nexport interface ReadGroup {\n key: string\n count: number\n sums: Record<string, number>\n}\n\n/** The sums of the number columns marked total - the server's, never the model's. */\nexport function sumsOf(target: ReadTarget, rows: Record<string, any>[]): Record<string, number> {\n const sums: Record<string, number> = {}\n for (const column of target.columns.filter(one => one.total && one.type === 'number')) {\n sums[column.name] = Math.round(rows.reduce((sum, row) => sum + (typeof row[column.name] === 'number' ? row[column.name] : 0), 0) * 100) / 100\n }\n return sums\n}\n\n/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */\nexport function groupsOf(target: ReadTarget, rows: Record<string, any>[], by: string): ReadGroup[] | undefined {\n const column = target.columns.find(one => one.name === by)\n if (!column) return undefined\n const groups = new Map<string, Record<string, any>[]>()\n for (const row of rows) {\n const value = row[by]\n const key = value === null || value === undefined ? '' : column.type === 'date' ? String(value).slice(0, 7) : String(value)\n groups.set(key, [...(groups.get(key) ?? []), row])\n }\n const list = [...groups].map(([key, members]) => ({ key, count: members.length, sums: sumsOf(target, members) }))\n const first = target.columns.find(one => one.total)?.name\n return column.type === 'date'\n ? list.sort((a, b) => a.key.localeCompare(b.key))\n : list.sort((a, b) => (first ? Math.abs(b.sums[first] ?? 0) - Math.abs(a.sums[first] ?? 0) : b.count - a.count))\n}\n\n/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */\nexport function screenAddress(target: ReadTarget, criteria: Record<string, unknown>): { href: string; applied: string[]; notApplied: string[] } | undefined {\n const screen = target.screen\n if (!screen) return undefined\n const params = new URLSearchParams()\n for (const [name, value] of Object.entries(screen.params(criteria))) if (value !== undefined && value !== null && value !== '') params.set(name, String(value))\n const given = Object.keys(criteria)\n const query = params.toString()\n return {\n href: query ? `${screen.route}?${query}` : screen.route,\n applied: given.filter(name => screen.takes.includes(name)),\n notApplied: given.filter(name => !screen.takes.includes(name))\n }\n}\n"]}
|