@carthooks/arcubase-cli 0.1.13 → 0.1.16
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/bundle/arcubase-admin.mjs +14934 -3808
- package/bundle/arcubase.mjs +14934 -3808
- package/dist/generated/command_registry.generated.d.ts +108 -0
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +145 -0
- package/dist/generated/zod_registry.generated.d.ts +23 -23
- package/dist/runtime/entity_import_mapping.d.ts +12 -0
- package/dist/runtime/entity_import_mapping.d.ts.map +1 -0
- package/dist/runtime/entity_import_mapping.js +133 -0
- package/dist/runtime/entity_save_schema.d.ts.map +1 -1
- package/dist/runtime/entity_save_schema.js +23 -2
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +309 -5
- package/dist/runtime/local_upload.d.ts +10 -0
- package/dist/runtime/local_upload.d.ts.map +1 -0
- package/dist/runtime/local_upload.js +73 -0
- package/dist/tests/entity_save_schema.test.js +38 -0
- package/dist/tests/execute_validation.test.js +2 -2
- package/package.json +7 -7
- package/sdk-dist/docs/runtime-reference/README.md +6 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +2 -2
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.query.json +1 -1
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +1 -1
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.query.json +1 -1
- package/sdk-dist/docs/runtime-reference/row-crud.md +55 -0
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +26 -1
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +1 -1
- package/sdk-dist/generated/command_registry.generated.ts +145 -0
- package/sdk-dist/types/common.ts +1 -0
- package/src/generated/command_registry.generated.ts +145 -0
- package/src/runtime/entity_import_mapping.ts +172 -0
- package/src/runtime/entity_save_schema.ts +27 -3
- package/src/runtime/execute.ts +324 -6
- package/src/runtime/local_upload.ts +84 -0
- package/src/tests/command_registry.test.ts +4 -0
- package/src/tests/entity_import_mapping.test.ts +87 -0
- package/src/tests/entity_save_schema.test.ts +40 -0
- package/src/tests/execute_validation.test.ts +231 -5
- package/src/tests/help.test.ts +14 -0
- package/src/tests/local_upload.test.ts +47 -0
- 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
|
-
"
|
|
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":
|
|
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",
|
package/sdk-dist/types/common.ts
CHANGED
|
@@ -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
|
|
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:
|
|
529
|
+
search: routeQuerySearchSchema.optional(),
|
|
506
530
|
sorts: z.array(queryOrderSchema).optional(),
|
|
507
531
|
view_mode: z.string().optional(),
|
|
508
532
|
viewMode: z.never().optional(),
|