@carthooks/arcubase-cli 0.1.13 → 0.1.15

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.
Files changed (41) hide show
  1. package/bundle/arcubase-admin.mjs +14934 -3808
  2. package/bundle/arcubase.mjs +14934 -3808
  3. package/dist/generated/command_registry.generated.d.ts +108 -0
  4. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  5. package/dist/generated/command_registry.generated.js +145 -0
  6. package/dist/generated/zod_registry.generated.d.ts +23 -23
  7. package/dist/runtime/entity_import_mapping.d.ts +12 -0
  8. package/dist/runtime/entity_import_mapping.d.ts.map +1 -0
  9. package/dist/runtime/entity_import_mapping.js +133 -0
  10. package/dist/runtime/entity_save_schema.d.ts.map +1 -1
  11. package/dist/runtime/entity_save_schema.js +23 -2
  12. package/dist/runtime/execute.d.ts.map +1 -1
  13. package/dist/runtime/execute.js +309 -5
  14. package/dist/runtime/local_upload.d.ts +10 -0
  15. package/dist/runtime/local_upload.d.ts.map +1 -0
  16. package/dist/runtime/local_upload.js +73 -0
  17. package/dist/tests/entity_save_schema.test.js +38 -0
  18. package/dist/tests/execute_validation.test.js +2 -2
  19. package/package.json +7 -7
  20. package/sdk-dist/docs/runtime-reference/README.md +4 -0
  21. package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +2 -2
  22. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.query.json +1 -1
  23. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +1 -1
  24. package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.query.json +1 -1
  25. package/sdk-dist/docs/runtime-reference/row-crud.md +55 -0
  26. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +26 -1
  27. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +1 -1
  28. package/sdk-dist/generated/command_registry.generated.ts +145 -0
  29. package/sdk-dist/types/common.ts +1 -0
  30. package/src/generated/command_registry.generated.ts +145 -0
  31. package/src/runtime/entity_import_mapping.ts +172 -0
  32. package/src/runtime/entity_save_schema.ts +27 -3
  33. package/src/runtime/execute.ts +324 -6
  34. package/src/runtime/local_upload.ts +84 -0
  35. package/src/tests/command_registry.test.ts +4 -0
  36. package/src/tests/entity_import_mapping.test.ts +87 -0
  37. package/src/tests/entity_save_schema.test.ts +40 -0
  38. package/src/tests/execute_validation.test.ts +230 -5
  39. package/src/tests/help.test.ts +14 -0
  40. package/src/tests/local_upload.test.ts +47 -0
  41. package/src/tests/zod_registry.test.ts +27 -0
@@ -152,6 +152,61 @@ Success result:
152
152
 
153
153
  The update command returns `true`, not the updated row body.
154
154
 
155
+ ## Import Records
156
+
157
+ Excel:
158
+
159
+ ```bash
160
+ arcubase row import-excel \
161
+ --app-id <app_id> \
162
+ --table-id <table_id> \
163
+ --file records.xlsx \
164
+ --mapping-file mapping.excel.json \
165
+ --ttl-seconds 300
166
+ ```
167
+
168
+ CSV:
169
+
170
+ ```bash
171
+ arcubase row import-csv \
172
+ --app-id <app_id> \
173
+ --table-id <table_id> \
174
+ --file records.csv \
175
+ --mapping-file mapping.csv.json \
176
+ --ttl-seconds 300
177
+ ```
178
+
179
+ Both commands accept local file paths only. CSV mapping uses headers. Excel mapping uses column letters. The mapping file is validated locally before the import task is started.
180
+
181
+ Excel `mapping.excel.json`:
182
+
183
+ ```json
184
+ {
185
+ "sheet": "Sheet1",
186
+ "mode": "addonly",
187
+ "headerRow": 1,
188
+ "dataStartRow": 2,
189
+ "fields": [
190
+ { "fieldId": 1001, "column": "A" }
191
+ ]
192
+ }
193
+ ```
194
+
195
+ CSV `mapping.csv.json`:
196
+
197
+ ```json
198
+ {
199
+ "mode": "addonly",
200
+ "header": true,
201
+ "dataStartRow": 2,
202
+ "fields": [
203
+ { "fieldId": 1001, "header": "客户名称" }
204
+ ]
205
+ }
206
+ ```
207
+
208
+ For `updateonly` and `addorupdate`, include `recordId` in the mapping. Use `arcubase-admin table get --app-id <app_id> --table-id <table_id>` to inspect numeric field ids.
209
+
155
210
  ## Delete rows
156
211
 
157
212
  Command:
@@ -38,13 +38,38 @@ For extra query-string parameters, also use:
38
38
  "limit": 20,
39
39
  "offset": 0,
40
40
  "search": {
41
- "text_search": "ORD-3001"
41
+ "q": "ORD-3001"
42
42
  }
43
43
  }
44
44
  ```
45
45
 
46
46
  This is the current success path for free-text search.
47
47
 
48
+ Rules:
49
+
50
+ - `search` is route-query state, not `ConditionSet`
51
+ - every `search` value must be a string
52
+ - use `q` for free-text search
53
+ - use `tab` for a list tab id
54
+ - use `filter_:<field_id_or_prop>` for field filters
55
+ - do not use `search.conditions`, `search.condition`, `search.field_filters`, `filter`, `filters`, `where`, `order`, or `orderBy`
56
+
57
+ ## Field filter
58
+
59
+ `query.json`:
60
+
61
+ ```json
62
+ {
63
+ "limit": 20,
64
+ "offset": 0,
65
+ "search": {
66
+ "filter_:1002": "%7B%22is%22%3A%22member_1%22%7D"
67
+ }
68
+ }
69
+ ```
70
+
71
+ `filter_:...` values are URL-encoded `FilterCond` JSON strings. For example, `%7B%22is%22%3A%22member_1%22%7D` decodes to `{"is":"member_1"}`.
72
+
48
73
  ## Sorts
49
74
 
50
75
  `query.json`:
@@ -102,7 +102,7 @@ Minimal `table-create.json`:
102
102
  "depends": [],
103
103
  "key": "description",
104
104
  "code_index": false,
105
- "options": {"placeholder": "", "html": false, "size": "default", "lengthLimit": false, "lengthMin": 0, "lengthMax": 2000},
105
+ "options": {"placeholder": "", "html": false, "size": 6, "lengthLimit": false, "lengthMin": 0, "lengthMax": 2000},
106
106
  "transfers": null,
107
107
  "children": null
108
108
  },
@@ -68,6 +68,63 @@ export const adminCommands = [
68
68
  "queryParams": [],
69
69
  "responseType": "AppCreateByTenantsRespVO"
70
70
  },
71
+ {
72
+ "scope": "admin",
73
+ "module": "app",
74
+ "functionName": "createAppFromBundleURL",
75
+ "commandPath": [
76
+ "app",
77
+ "create-from-bundle-url"
78
+ ],
79
+ "method": "POST",
80
+ "endpoint": "/api/apps/import-task",
81
+ "pathParams": [],
82
+ "controller": "manual",
83
+ "requestType": null,
84
+ "queryParams": [
85
+ {
86
+ "key": "url",
87
+ "flag": "url",
88
+ "type": "string",
89
+ "hasDefault": false
90
+ },
91
+ {
92
+ "key": "name",
93
+ "flag": "name",
94
+ "type": "string",
95
+ "hasDefault": true
96
+ }
97
+ ],
98
+ "responseType": "AppBundleImportTaskCreateRespVO"
99
+ },
100
+ {
101
+ "scope": "admin",
102
+ "module": "app",
103
+ "functionName": "watchAppBundleImportTask",
104
+ "commandPath": [
105
+ "app",
106
+ "watch-import-task"
107
+ ],
108
+ "method": "GET",
109
+ "endpoint": "/api/apps/import-task/:task_id",
110
+ "pathParams": [
111
+ {
112
+ "param": "task_id",
113
+ "flag": "task-id"
114
+ }
115
+ ],
116
+ "controller": "manual",
117
+ "requestType": null,
118
+ "queryParams": [
119
+ {
120
+ "key": "ttl_seconds",
121
+ "flag": "ttl-seconds",
122
+ "type": "number",
123
+ "hasDefault": false
124
+ }
125
+ ],
126
+ "responseType": "AppBundleImportTaskStatusRespVO"
127
+ },
71
128
  {
72
129
  "scope": "admin",
73
130
  "module": "app",
@@ -791,6 +848,94 @@ export const userCommands = [
791
848
  "queryParams": [],
792
849
  "responseType": "EntityBulkUpdateRespVO"
793
850
  },
851
+ {
852
+ "scope": "user",
853
+ "module": "row",
854
+ "functionName": "importEntityRecordsFromExcel",
855
+ "commandPath": [
856
+ "row",
857
+ "import-excel"
858
+ ],
859
+ "method": "POST",
860
+ "endpoint": "/api/entity/:app_id/:entity_id/import",
861
+ "pathParams": [
862
+ {
863
+ "param": "app_id",
864
+ "flag": "app-id"
865
+ },
866
+ {
867
+ "param": "entity_id",
868
+ "flag": "table-id"
869
+ }
870
+ ],
871
+ "controller": "manual",
872
+ "requestType": null,
873
+ "queryParams": [
874
+ {
875
+ "key": "file",
876
+ "flag": "file",
877
+ "type": "string",
878
+ "hasDefault": false
879
+ },
880
+ {
881
+ "key": "mapping_file",
882
+ "flag": "mapping-file",
883
+ "type": "string",
884
+ "hasDefault": false
885
+ },
886
+ {
887
+ "key": "ttl_seconds",
888
+ "flag": "ttl-seconds",
889
+ "type": "number",
890
+ "hasDefault": true
891
+ }
892
+ ],
893
+ "responseType": "ImporterGetStateRespVO"
894
+ },
895
+ {
896
+ "scope": "user",
897
+ "module": "row",
898
+ "functionName": "importEntityRecordsFromCSV",
899
+ "commandPath": [
900
+ "row",
901
+ "import-csv"
902
+ ],
903
+ "method": "POST",
904
+ "endpoint": "/api/entity/:app_id/:entity_id/import",
905
+ "pathParams": [
906
+ {
907
+ "param": "app_id",
908
+ "flag": "app-id"
909
+ },
910
+ {
911
+ "param": "entity_id",
912
+ "flag": "table-id"
913
+ }
914
+ ],
915
+ "controller": "manual",
916
+ "requestType": null,
917
+ "queryParams": [
918
+ {
919
+ "key": "file",
920
+ "flag": "file",
921
+ "type": "string",
922
+ "hasDefault": false
923
+ },
924
+ {
925
+ "key": "mapping_file",
926
+ "flag": "mapping-file",
927
+ "type": "string",
928
+ "hasDefault": false
929
+ },
930
+ {
931
+ "key": "ttl_seconds",
932
+ "flag": "ttl-seconds",
933
+ "type": "number",
934
+ "hasDefault": true
935
+ }
936
+ ],
937
+ "responseType": "ImporterGetStateRespVO"
938
+ },
794
939
  {
795
940
  "scope": "user",
796
941
  "module": "row",
@@ -293,6 +293,7 @@ export interface KioskConfig {
293
293
  interactiveMode?: string;
294
294
  lookupFields?: number[];
295
295
  options?: Record<string, any>;
296
+ printers?: Record<string, any>[];
296
297
  screens?: KioskScreen[];
297
298
  theme?: string;
298
299
  }
@@ -68,6 +68,63 @@ export const adminCommands = [
68
68
  "queryParams": [],
69
69
  "responseType": "AppCreateByTenantsRespVO"
70
70
  },
71
+ {
72
+ "scope": "admin",
73
+ "module": "app",
74
+ "functionName": "createAppFromBundleURL",
75
+ "commandPath": [
76
+ "app",
77
+ "create-from-bundle-url"
78
+ ],
79
+ "method": "POST",
80
+ "endpoint": "/api/apps/import-task",
81
+ "pathParams": [],
82
+ "controller": "manual",
83
+ "requestType": null,
84
+ "queryParams": [
85
+ {
86
+ "key": "url",
87
+ "flag": "url",
88
+ "type": "string",
89
+ "hasDefault": false
90
+ },
91
+ {
92
+ "key": "name",
93
+ "flag": "name",
94
+ "type": "string",
95
+ "hasDefault": true
96
+ }
97
+ ],
98
+ "responseType": "AppBundleImportTaskCreateRespVO"
99
+ },
100
+ {
101
+ "scope": "admin",
102
+ "module": "app",
103
+ "functionName": "watchAppBundleImportTask",
104
+ "commandPath": [
105
+ "app",
106
+ "watch-import-task"
107
+ ],
108
+ "method": "GET",
109
+ "endpoint": "/api/apps/import-task/:task_id",
110
+ "pathParams": [
111
+ {
112
+ "param": "task_id",
113
+ "flag": "task-id"
114
+ }
115
+ ],
116
+ "controller": "manual",
117
+ "requestType": null,
118
+ "queryParams": [
119
+ {
120
+ "key": "ttl_seconds",
121
+ "flag": "ttl-seconds",
122
+ "type": "number",
123
+ "hasDefault": false
124
+ }
125
+ ],
126
+ "responseType": "AppBundleImportTaskStatusRespVO"
127
+ },
71
128
  {
72
129
  "scope": "admin",
73
130
  "module": "app",
@@ -791,6 +848,94 @@ export const userCommands = [
791
848
  "queryParams": [],
792
849
  "responseType": "EntityBulkUpdateRespVO"
793
850
  },
851
+ {
852
+ "scope": "user",
853
+ "module": "row",
854
+ "functionName": "importEntityRecordsFromExcel",
855
+ "commandPath": [
856
+ "row",
857
+ "import-excel"
858
+ ],
859
+ "method": "POST",
860
+ "endpoint": "/api/entity/:app_id/:entity_id/import",
861
+ "pathParams": [
862
+ {
863
+ "param": "app_id",
864
+ "flag": "app-id"
865
+ },
866
+ {
867
+ "param": "entity_id",
868
+ "flag": "table-id"
869
+ }
870
+ ],
871
+ "controller": "manual",
872
+ "requestType": null,
873
+ "queryParams": [
874
+ {
875
+ "key": "file",
876
+ "flag": "file",
877
+ "type": "string",
878
+ "hasDefault": false
879
+ },
880
+ {
881
+ "key": "mapping_file",
882
+ "flag": "mapping-file",
883
+ "type": "string",
884
+ "hasDefault": false
885
+ },
886
+ {
887
+ "key": "ttl_seconds",
888
+ "flag": "ttl-seconds",
889
+ "type": "number",
890
+ "hasDefault": true
891
+ }
892
+ ],
893
+ "responseType": "ImporterGetStateRespVO"
894
+ },
895
+ {
896
+ "scope": "user",
897
+ "module": "row",
898
+ "functionName": "importEntityRecordsFromCSV",
899
+ "commandPath": [
900
+ "row",
901
+ "import-csv"
902
+ ],
903
+ "method": "POST",
904
+ "endpoint": "/api/entity/:app_id/:entity_id/import",
905
+ "pathParams": [
906
+ {
907
+ "param": "app_id",
908
+ "flag": "app-id"
909
+ },
910
+ {
911
+ "param": "entity_id",
912
+ "flag": "table-id"
913
+ }
914
+ ],
915
+ "controller": "manual",
916
+ "requestType": null,
917
+ "queryParams": [
918
+ {
919
+ "key": "file",
920
+ "flag": "file",
921
+ "type": "string",
922
+ "hasDefault": false
923
+ },
924
+ {
925
+ "key": "mapping_file",
926
+ "flag": "mapping-file",
927
+ "type": "string",
928
+ "hasDefault": false
929
+ },
930
+ {
931
+ "key": "ttl_seconds",
932
+ "flag": "ttl-seconds",
933
+ "type": "number",
934
+ "hasDefault": true
935
+ }
936
+ ],
937
+ "responseType": "ImporterGetStateRespVO"
938
+ },
794
939
  {
795
940
  "scope": "user",
796
941
  "module": "row",
@@ -0,0 +1,172 @@
1
+ import { CLIError } from './errors.js'
2
+
3
+ export type ImportMode = 'addonly' | 'updateonly' | 'addorupdate'
4
+
5
+ export type CompiledEntityImportConfig = {
6
+ fileFormat: 'excel' | 'csv'
7
+ importMode: ImportMode
8
+ workingSheet?: string
9
+ titleRowIndex: number
10
+ csvDelimiter?: string
11
+ mapper: Record<string, number>
12
+ }
13
+
14
+ type ExcelMapping = {
15
+ sheet: unknown
16
+ mode: unknown
17
+ headerRow?: unknown
18
+ dataStartRow: unknown
19
+ recordId?: { column?: unknown }
20
+ fields: Array<{ fieldId?: unknown; column?: unknown }>
21
+ }
22
+
23
+ type CSVMapping = {
24
+ mode: unknown
25
+ header: unknown
26
+ dataStartRow: unknown
27
+ delimiter?: unknown
28
+ recordId?: { header?: unknown }
29
+ fields: Array<{ fieldId?: unknown; header?: unknown }>
30
+ }
31
+
32
+ function fail(message: string, details: Record<string, unknown> = {}): never {
33
+ throw new CLIError('INVALID_IMPORT_MAPPING', message, 2, {
34
+ hint: 'run command help for a complete mapping example',
35
+ ...details,
36
+ })
37
+ }
38
+
39
+ function parseMode(value: unknown): ImportMode {
40
+ if (value === 'addonly' || value === 'updateonly' || value === 'addorupdate') return value
41
+ fail('mode must be one of addonly, updateonly, addorupdate')
42
+ }
43
+
44
+ function parseDataStartRow(value: unknown): number {
45
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 2) {
46
+ fail('dataStartRow must be an integer greater than or equal to 2')
47
+ }
48
+ return value
49
+ }
50
+
51
+ function titleRowIndexFromDataStartRow(value: number): number {
52
+ return value - 2
53
+ }
54
+
55
+ function parseFieldId(value: unknown, path: string): number {
56
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
57
+ fail(`${path}.fieldId must be a number`, {
58
+ commonMistakes: ['fieldId must be numeric, not a quoted string', 'use arcubase-admin table get to inspect field ids'],
59
+ })
60
+ }
61
+ return value
62
+ }
63
+
64
+ function excelColumnToIndex(column: unknown, path: string): number {
65
+ if (typeof column !== 'string' || !/^[A-Za-z]+$/.test(column.trim())) {
66
+ fail(`${path} must be an Excel column such as A, B, or AA`)
67
+ }
68
+ let value = 0
69
+ for (const char of column.trim().toUpperCase()) {
70
+ value = value * 26 + (char.charCodeAt(0) - 64)
71
+ }
72
+ return value - 1
73
+ }
74
+
75
+ function assertUniqueTarget(mapper: Record<string, number>, key: string, index: number) {
76
+ if (key in mapper) {
77
+ fail(`duplicate field mapping for ${key}`)
78
+ }
79
+ if (Object.values(mapper).includes(index)) {
80
+ fail(`duplicate source column index ${index}`)
81
+ }
82
+ mapper[key] = index
83
+ }
84
+
85
+ function validateRecordId(mode: ImportMode, recordId: unknown) {
86
+ if ((mode === 'updateonly' || mode === 'addorupdate') && !recordId) {
87
+ fail('recordId is required for updateonly and addorupdate')
88
+ }
89
+ }
90
+
91
+ export function compileExcelImportMapping(input: unknown): CompiledEntityImportConfig {
92
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
93
+ fail('excel mapping must be a JSON object')
94
+ }
95
+ const mapping = input as ExcelMapping
96
+ const mode = parseMode(mapping.mode)
97
+ validateRecordId(mode, mapping.recordId)
98
+ if (typeof mapping.sheet !== 'string' || mapping.sheet.trim() === '') {
99
+ fail('sheet is required for excel import')
100
+ }
101
+ if (!Array.isArray(mapping.fields) || mapping.fields.length === 0) {
102
+ fail('fields must be a non-empty array')
103
+ }
104
+ const mapper: Record<string, number> = {}
105
+ if (mapping.recordId?.column !== undefined) {
106
+ assertUniqueTarget(mapper, '$id', excelColumnToIndex(mapping.recordId.column, 'recordId.column'))
107
+ }
108
+ mapping.fields.forEach((field, index) => {
109
+ const fieldId = parseFieldId(field.fieldId, `fields.${index}`)
110
+ const columnIndex = excelColumnToIndex(field.column, `fields.${index}.column`)
111
+ assertUniqueTarget(mapper, String(fieldId), columnIndex)
112
+ })
113
+ return {
114
+ fileFormat: 'excel',
115
+ importMode: mode,
116
+ workingSheet: mapping.sheet.trim(),
117
+ titleRowIndex: titleRowIndexFromDataStartRow(parseDataStartRow(mapping.dataStartRow)),
118
+ mapper,
119
+ }
120
+ }
121
+
122
+ function parseDelimiter(value: unknown): string {
123
+ if (value === undefined || value === null || value === '') return ','
124
+ if (typeof value !== 'string' || [...value].length !== 1) {
125
+ fail('delimiter must be exactly one character')
126
+ }
127
+ return value
128
+ }
129
+
130
+ function headerIndex(headers: string[], wanted: unknown, path: string): number {
131
+ if (typeof wanted !== 'string' || wanted.trim() === '') {
132
+ fail(`${path} is required`)
133
+ }
134
+ const index = headers.indexOf(wanted)
135
+ if (index < 0) {
136
+ fail(`CSV header not found: ${wanted}`, {
137
+ suggestions: [`available headers: ${headers.join(', ')}`],
138
+ })
139
+ }
140
+ return index
141
+ }
142
+
143
+ export function compileCSVImportMapping(input: unknown, headers: string[]): CompiledEntityImportConfig {
144
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
145
+ fail('csv mapping must be a JSON object')
146
+ }
147
+ const mapping = input as CSVMapping
148
+ const mode = parseMode(mapping.mode)
149
+ validateRecordId(mode, mapping.recordId)
150
+ if (mapping.header !== true) {
151
+ fail('csv mapping header must be true')
152
+ }
153
+ if (!Array.isArray(mapping.fields) || mapping.fields.length === 0) {
154
+ fail('fields must be a non-empty array')
155
+ }
156
+ const mapper: Record<string, number> = {}
157
+ if (mapping.recordId?.header !== undefined) {
158
+ assertUniqueTarget(mapper, '$id', headerIndex(headers, mapping.recordId.header, 'recordId.header'))
159
+ }
160
+ mapping.fields.forEach((field, index) => {
161
+ const fieldId = parseFieldId(field.fieldId, `fields.${index}`)
162
+ const columnIndex = headerIndex(headers, field.header, `fields.${index}.header`)
163
+ assertUniqueTarget(mapper, String(fieldId), columnIndex)
164
+ })
165
+ return {
166
+ fileFormat: 'csv',
167
+ importMode: mode,
168
+ titleRowIndex: titleRowIndexFromDataStartRow(parseDataStartRow(mapping.dataStartRow)),
169
+ csvDelimiter: parseDelimiter(mapping.delimiter),
170
+ mapper,
171
+ }
172
+ }
@@ -197,6 +197,15 @@ function makeSimpleFieldSchema(type: string) {
197
197
  }).strict()
198
198
  }
199
199
 
200
+ const textareaFieldSchema = z.object({
201
+ ...fieldCommonShape,
202
+ type: z.literal('textarea'),
203
+ options: z.object({
204
+ size: z.number().int().min(1),
205
+ }).passthrough(),
206
+ children: z.null(),
207
+ }).strict()
208
+
200
209
  function makeEnumFieldSchema(type: typeof enumFieldTypes[number]) {
201
210
  const itemSchema = type === 'status' ? statusItemSchema : enumItemSchema
202
211
  return z.object({
@@ -211,7 +220,9 @@ function makeEnumFieldSchema(type: typeof enumFieldTypes[number]) {
211
220
  }).strict()
212
221
  }
213
222
 
214
- const simpleFieldSchemas = simpleFieldTypes.map((type) => makeSimpleFieldSchema(type))
223
+ const simpleFieldSchemas = simpleFieldTypes
224
+ .filter((type) => type !== 'textarea')
225
+ .map((type) => makeSimpleFieldSchema(type))
215
226
  const enumFieldSchemas = enumFieldTypes.map((type) => makeEnumFieldSchema(type))
216
227
 
217
228
  const formulaFieldSchema = z.object({
@@ -290,6 +301,7 @@ const genericFieldSchema = z.object({
290
301
  }).strict()
291
302
 
292
303
  const baseFieldSchemaVariants = [
304
+ textareaFieldSchema,
293
305
  ...simpleFieldSchemas,
294
306
  ...enumFieldSchemas,
295
307
  ...relationFieldSchemas,
@@ -395,7 +407,7 @@ const EntitySaveReqVOBaseSchema = z.object({
395
407
  schema_version: z.number().optional(),
396
408
  workflow_enabled: z.boolean().optional(),
397
409
  }).strict().superRefine((entity, ctx) => {
398
- const fields = entity.fields
410
+ const fields = entity.fields as any[]
399
411
  const topLevelIds = new Set<number>(collectTopLevelFieldIds(fields))
400
412
  const allIds = collectAllFieldIds(fields)
401
413
  const allIdSet = new Set<number>()
@@ -492,6 +504,18 @@ const queryOrderSchema = z.object({
492
504
  order: z.string().min(1),
493
505
  }).strict()
494
506
 
507
+ const routeQuerySearchKeySchema = z.string().superRefine((key, ctx) => {
508
+ if (key === 'q' || key === 'tab' || /^filter_:\d+$/.test(key)) {
509
+ return
510
+ }
511
+ ctx.addIssue({
512
+ code: z.ZodIssueCode.custom,
513
+ message: 'row query search only accepts route-query string keys: q, tab, and filter_:<field_id>',
514
+ })
515
+ })
516
+
517
+ const routeQuerySearchSchema = z.record(routeQuerySearchKeySchema, z.string().min(1))
518
+
495
519
  export const AppCreateByTenantsReqVOSchemaOverride: z.ZodTypeAny = z.object({
496
520
  icon_color: z.string().optional(),
497
521
  icon_name: z.string().optional(),
@@ -502,7 +526,7 @@ export const EntityQueryReqVOSchemaOverride: z.ZodTypeAny = z.object({
502
526
  limit: z.number().int().positive().optional(),
503
527
  offset: z.number().int().min(0).optional(),
504
528
  policy_id: z.string().optional(),
505
- search: z.record(z.string(), z.any()).optional(),
529
+ search: routeQuerySearchSchema.optional(),
506
530
  sorts: z.array(queryOrderSchema).optional(),
507
531
  view_mode: z.string().optional(),
508
532
  viewMode: z.never().optional(),