@carthooks/arcubase-cli 0.1.24 → 0.1.26
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 +527 -183
- package/bundle/arcubase.mjs +527 -183
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +3 -3
- package/dist/runtime/dev_sdk_gen.d.ts.map +1 -1
- package/dist/runtime/dev_sdk_gen.js +423 -144
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +122 -48
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/dev-sdk-gen.md +27 -12
- package/sdk-dist/generated/zod_registry.generated.ts +3 -3
- package/sdk-dist/types/common.ts +1 -0
- package/sdk-dist/types/ingress.ts +15 -0
- package/sdk-dist/types/user-action.ts +1 -0
- package/src/generated/zod_registry.generated.ts +3 -3
- package/src/runtime/dev_sdk_gen.ts +453 -147
- package/src/runtime/execute.ts +128 -51
- package/src/tests/dev_sdk_gen.test.ts +134 -61
- package/src/tests/help.test.ts +2 -1
- package/src/tests/zod_registry.test.ts +1 -0
package/src/runtime/execute.ts
CHANGED
|
@@ -295,10 +295,10 @@ function renderDevSDKGenHelp(): string {
|
|
|
295
295
|
' - arcubase-admin dev sdk-gen --app-id <app_id> --out src/arcubase',
|
|
296
296
|
'',
|
|
297
297
|
'requirements:',
|
|
298
|
-
' -
|
|
299
|
-
' -
|
|
300
|
-
' -
|
|
301
|
-
' - generated code accepts
|
|
298
|
+
' - every generated ingress must have a configured ingress.key; sdk-gen fails fast when keys are missing',
|
|
299
|
+
' - every generated field must have a configured field.key; sdk-gen fails fast when keys are missing',
|
|
300
|
+
' - ingress.key and field.key values must be stable unique TypeScript identifiers',
|
|
301
|
+
' - generated code accepts createArcubaseClient({ baseURL, accessToken, refreshToken }).ingress("<IngressKey>")',
|
|
302
302
|
].join('\n')
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -2006,10 +2006,7 @@ type SDKGenEntityRef = {
|
|
|
2006
2006
|
id: string
|
|
2007
2007
|
}
|
|
2008
2008
|
|
|
2009
|
-
type
|
|
2010
|
-
entityId: number
|
|
2011
|
-
fields: string[]
|
|
2012
|
-
}
|
|
2009
|
+
type SDKGenIngressRef = Record<string, any>
|
|
2013
2010
|
|
|
2014
2011
|
function extractSDKGenAppPayload(payload: unknown): Record<string, any> {
|
|
2015
2012
|
const appPayload = unwrapResponseData(payload)
|
|
@@ -2056,47 +2053,128 @@ function walkSDKGenFields(fields: unknown, visit: (field: Record<string, any>) =
|
|
|
2056
2053
|
}
|
|
2057
2054
|
}
|
|
2058
2055
|
|
|
2059
|
-
function
|
|
2060
|
-
const
|
|
2056
|
+
function assertSDKGenFieldKeysPresent(entity: Record<string, any>) {
|
|
2057
|
+
const missing: Array<{ id: number; label: string }> = []
|
|
2061
2058
|
walkSDKGenFields(entity.fields, (field) => {
|
|
2059
|
+
if (typeof field.id !== 'number') {
|
|
2060
|
+
return
|
|
2061
|
+
}
|
|
2062
2062
|
if (typeof field.key === 'string' && field.key.trim() !== '') {
|
|
2063
|
-
|
|
2063
|
+
return
|
|
2064
2064
|
}
|
|
2065
|
+
missing.push({
|
|
2066
|
+
id: field.id,
|
|
2067
|
+
label: typeof field.label === 'string' && field.label.trim() ? field.label.trim() : `field ${field.id}`,
|
|
2068
|
+
})
|
|
2069
|
+
})
|
|
2070
|
+
if (missing.length === 0) {
|
|
2071
|
+
return
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
const entityName = typeof entity.name === 'string' && entity.name.trim() ? entity.name.trim() : `entity ${String(entity.id ?? '')}`.trim()
|
|
2075
|
+
const preview = missing.slice(0, 8).map((field) => `${entityName}.${field.label} (${field.id})`)
|
|
2076
|
+
const suffix = missing.length > preview.length ? `, and ${missing.length - preview.length} more` : ''
|
|
2077
|
+
throw new CLIError('SDK_GEN_FIELD_KEY_REQUIRED', `missing field.key values: ${preview.join(', ')}${suffix}`, 2, {
|
|
2078
|
+
operation: 'dev sdk-gen',
|
|
2079
|
+
issues: missing.map((field) => ({
|
|
2080
|
+
path: `entity.${String(entity.id ?? 'unknown')}.field.${field.id}.key`,
|
|
2081
|
+
message: `field.key is required for ${entityName}.${field.label}`,
|
|
2082
|
+
})),
|
|
2083
|
+
suggestions: [
|
|
2084
|
+
'set stable field.key values in Arcubase admin before running sdk-gen',
|
|
2085
|
+
'rerun arcubase-admin dev sdk-gen after the schema keys are complete',
|
|
2086
|
+
],
|
|
2065
2087
|
})
|
|
2066
|
-
return keys
|
|
2067
2088
|
}
|
|
2068
2089
|
|
|
2069
|
-
function
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2090
|
+
function extractSDKGenIngressRefs(payload: unknown): SDKGenIngressRef[] {
|
|
2091
|
+
const data = unwrapResponseData(payload)
|
|
2092
|
+
const source = Array.isArray(data)
|
|
2093
|
+
? data
|
|
2094
|
+
: isRecord(data) && Array.isArray(data.list)
|
|
2095
|
+
? data.list
|
|
2096
|
+
: []
|
|
2097
|
+
return source.filter((item): item is SDKGenIngressRef => isRecord(item))
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
function ingressKeyOf(ingress: Record<string, any>): string {
|
|
2101
|
+
return typeof ingress.key === 'string' ? ingress.key.trim() : ''
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function ingressHashIDOf(ingress: Record<string, any>): string {
|
|
2105
|
+
if (typeof ingress.hash_id === 'string' && ingress.hash_id.trim()) {
|
|
2106
|
+
return ingress.hash_id.trim()
|
|
2074
2107
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
index++
|
|
2108
|
+
if (typeof ingress.hashId === 'string' && ingress.hashId.trim()) {
|
|
2109
|
+
return ingress.hashId.trim()
|
|
2078
2110
|
}
|
|
2079
|
-
|
|
2080
|
-
usedKeys.add(key)
|
|
2081
|
-
return key
|
|
2111
|
+
return ''
|
|
2082
2112
|
}
|
|
2083
2113
|
|
|
2084
|
-
function
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2114
|
+
function assertSDKGenIngressesReady(ingresses: SDKGenIngressRef[]) {
|
|
2115
|
+
if (ingresses.length === 0) {
|
|
2116
|
+
throw new CLIError('SDK_GEN_NO_INGRESSES', 'sdk-gen could not find ingress definitions for this app', 2, {
|
|
2117
|
+
operation: 'dev sdk-gen',
|
|
2118
|
+
suggestions: ['create at least one access rule with a stable ingress.key before running sdk-gen'],
|
|
2119
|
+
})
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const seen = new Set<string>()
|
|
2123
|
+
const missingKey: SDKGenIngressRef[] = []
|
|
2124
|
+
const missingHash: SDKGenIngressRef[] = []
|
|
2125
|
+
const invalidKey: string[] = []
|
|
2126
|
+
const duplicateKey: string[] = []
|
|
2127
|
+
|
|
2128
|
+
for (const ingress of ingresses) {
|
|
2129
|
+
const key = ingressKeyOf(ingress)
|
|
2130
|
+
if (!key) {
|
|
2131
|
+
missingKey.push(ingress)
|
|
2132
|
+
continue
|
|
2090
2133
|
}
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
return
|
|
2134
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key)) {
|
|
2135
|
+
invalidKey.push(key)
|
|
2094
2136
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2137
|
+
if (seen.has(key)) {
|
|
2138
|
+
duplicateKey.push(key)
|
|
2139
|
+
}
|
|
2140
|
+
seen.add(key)
|
|
2141
|
+
if (!ingressHashIDOf(ingress)) {
|
|
2142
|
+
missingHash.push(ingress)
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (missingKey.length > 0) {
|
|
2147
|
+
const preview = missingKey.slice(0, 8).map((ingress) => String(ingress.name ?? ingress.id ?? 'unnamed ingress'))
|
|
2148
|
+
const suffix = missingKey.length > preview.length ? `, and ${missingKey.length - preview.length} more` : ''
|
|
2149
|
+
throw new CLIError('SDK_GEN_INGRESS_KEY_REQUIRED', `missing ingress.key values: ${preview.join(', ')}${suffix}`, 2, {
|
|
2150
|
+
operation: 'dev sdk-gen',
|
|
2151
|
+
issues: missingKey.map((ingress) => ({
|
|
2152
|
+
path: `ingress.${String(ingress.id ?? 'unknown')}.key`,
|
|
2153
|
+
message: `ingress.key is required for ${String(ingress.name ?? ingress.id ?? 'unnamed ingress')}`,
|
|
2154
|
+
})),
|
|
2155
|
+
suggestions: [
|
|
2156
|
+
'set stable ingress.key values in Arcubase admin before running sdk-gen',
|
|
2157
|
+
'rerun arcubase-admin dev sdk-gen after ingress keys are complete',
|
|
2158
|
+
],
|
|
2159
|
+
})
|
|
2160
|
+
}
|
|
2161
|
+
if (invalidKey.length > 0) {
|
|
2162
|
+
throw new CLIError('SDK_GEN_INVALID_INGRESS_KEY', `invalid ingress.key values: ${invalidKey.join(', ')}`, 2, {
|
|
2163
|
+
operation: 'dev sdk-gen',
|
|
2164
|
+
suggestions: ['use TypeScript identifier keys such as publicEntry or customerPortal'],
|
|
2165
|
+
})
|
|
2166
|
+
}
|
|
2167
|
+
if (duplicateKey.length > 0) {
|
|
2168
|
+
throw new CLIError('SDK_GEN_DUPLICATE_INGRESS_KEY', `duplicate ingress.key values: ${duplicateKey.join(', ')}`, 2, {
|
|
2169
|
+
operation: 'dev sdk-gen',
|
|
2170
|
+
})
|
|
2171
|
+
}
|
|
2172
|
+
if (missingHash.length > 0) {
|
|
2173
|
+
throw new CLIError('SDK_GEN_INGRESS_HASH_ID_REQUIRED', 'sdk-gen expected hash_id for every ingress definition', 2, {
|
|
2174
|
+
operation: 'dev sdk-gen',
|
|
2175
|
+
suggestions: ['upgrade Arcubase server so admin access-rule list returns hash_id'],
|
|
2176
|
+
})
|
|
2177
|
+
}
|
|
2100
2178
|
}
|
|
2101
2179
|
|
|
2102
2180
|
async function executeDevSDKGen(
|
|
@@ -2130,8 +2208,7 @@ async function executeDevSDKGen(
|
|
|
2130
2208
|
if (entityRefs.length === 0) {
|
|
2131
2209
|
throw new CLIError('SDK_GEN_NO_ENTITIES', 'sdk-gen could not find entities in app detail response', 2)
|
|
2132
2210
|
}
|
|
2133
|
-
const
|
|
2134
|
-
const initializedFieldKeys: SDKGenInitializedFieldKeys[] = []
|
|
2211
|
+
const ingresses: SDKGenIngressRef[] = []
|
|
2135
2212
|
for (const entityRef of entityRefs) {
|
|
2136
2213
|
const entityEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}`
|
|
2137
2214
|
const entityDetail = await requestJSON(runtimeEnv, 'GET', entityEndpoint, undefined, fetchImpl)
|
|
@@ -2139,21 +2216,22 @@ async function executeDevSDKGen(
|
|
|
2139
2216
|
if (!isRecord(entityPayload)) {
|
|
2140
2217
|
throw new CLIError('SDK_GEN_INVALID_SCHEMA', 'sdk-gen expected entity detail response data', 2)
|
|
2141
2218
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2219
|
+
assertSDKGenFieldKeysPresent(entityPayload)
|
|
2220
|
+
const ingressEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}/ingress`
|
|
2221
|
+
const ingressList = await requestJSON(runtimeEnv, 'GET', ingressEndpoint, undefined, fetchImpl)
|
|
2222
|
+
const entityIngresses = extractSDKGenIngressRefs(ingressList.data).map((ingress) => ({
|
|
2223
|
+
...ingress,
|
|
2224
|
+
hash_id: ingressHashIDOf(ingress),
|
|
2225
|
+
key: ingressKeyOf(ingress),
|
|
2226
|
+
entity: entityPayload,
|
|
2227
|
+
}))
|
|
2228
|
+
ingresses.push(...entityIngresses)
|
|
2152
2229
|
}
|
|
2230
|
+
assertSDKGenIngressesReady(ingresses)
|
|
2153
2231
|
const generated = generateArcubaseProjectSDK({
|
|
2154
2232
|
id: resolveSDKGenAppId(appId, appPayload),
|
|
2155
2233
|
name: typeof appPayload.name === 'string' ? appPayload.name : undefined,
|
|
2156
|
-
|
|
2234
|
+
ingresses,
|
|
2157
2235
|
})
|
|
2158
2236
|
const absoluteOutDir = path.resolve(process.cwd(), outDir)
|
|
2159
2237
|
|
|
@@ -2173,7 +2251,6 @@ async function executeDevSDKGen(
|
|
|
2173
2251
|
data: {
|
|
2174
2252
|
out: absoluteOutDir,
|
|
2175
2253
|
files: generated.files.map((file) => file.path),
|
|
2176
|
-
initializedFieldKeys,
|
|
2177
2254
|
},
|
|
2178
2255
|
}
|
|
2179
2256
|
}
|
|
@@ -21,7 +21,7 @@ const env = {
|
|
|
21
21
|
function appDetail() {
|
|
22
22
|
return {
|
|
23
23
|
id: 3882856425,
|
|
24
|
-
name: '
|
|
24
|
+
name: 'Demo App',
|
|
25
25
|
entities: {},
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -30,7 +30,7 @@ function entityList() {
|
|
|
30
30
|
return [
|
|
31
31
|
{
|
|
32
32
|
id: 3883547182,
|
|
33
|
-
name: '
|
|
33
|
+
name: 'Records',
|
|
34
34
|
},
|
|
35
35
|
]
|
|
36
36
|
}
|
|
@@ -42,7 +42,7 @@ function appDetailWithEntityList() {
|
|
|
42
42
|
entities: [
|
|
43
43
|
{
|
|
44
44
|
id: 3883547182,
|
|
45
|
-
name: '
|
|
45
|
+
name: 'Records',
|
|
46
46
|
},
|
|
47
47
|
],
|
|
48
48
|
}
|
|
@@ -52,12 +52,11 @@ function entityDetail() {
|
|
|
52
52
|
return {
|
|
53
53
|
id: 3883547182,
|
|
54
54
|
app_id: 3882856425,
|
|
55
|
-
name: '
|
|
56
|
-
key: 'story',
|
|
55
|
+
name: 'Records',
|
|
57
56
|
fields: [
|
|
58
|
-
{ id: 1006, label: '
|
|
59
|
-
{ id: 1013, label: '
|
|
60
|
-
{ id: 1011, label: '
|
|
57
|
+
{ id: 1006, label: 'Title', key: 'title', type: 'text', required: true, options: {} },
|
|
58
|
+
{ id: 1013, label: 'Status', key: 'status', type: 'select', required: false, options: { options: { items: [{ key: 1, value: 'Open' }] } } },
|
|
59
|
+
{ id: 1011, label: 'Notes', key: 'notes', type: 'textarea', required: false, options: {} },
|
|
61
60
|
],
|
|
62
61
|
}
|
|
63
62
|
}
|
|
@@ -70,76 +69,125 @@ function entityDetailWithoutFieldKeys() {
|
|
|
70
69
|
|
|
71
70
|
function sdkSchemaPayload() {
|
|
72
71
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
id: 3882856425,
|
|
73
|
+
name: 'Demo App',
|
|
74
|
+
ingresses: [ingressDefinition()],
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ingressDefinition(actions = ['view', 'add', 'edit', 'delete', 'bulk_update', 'export', 'batch_print', 'tag_manage', 'archive']) {
|
|
79
|
+
return {
|
|
80
|
+
id: 9001,
|
|
81
|
+
hash_id: '8WR5Kypbw9KQK20',
|
|
82
|
+
key: 'publicEntry',
|
|
83
|
+
name: 'Public entry',
|
|
84
|
+
entity: entityDetail(),
|
|
85
|
+
options: {
|
|
86
|
+
policy: {
|
|
87
|
+
actions,
|
|
88
|
+
all_fields_read: true,
|
|
89
|
+
all_fields_write: true,
|
|
90
|
+
fields: [],
|
|
78
91
|
},
|
|
79
|
-
|
|
92
|
+
list_options: {},
|
|
93
|
+
},
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
test('generateArcubaseProjectSDK emits typed
|
|
97
|
+
test('generateArcubaseProjectSDK emits typed ingress sdk selected from client.ingress', () => {
|
|
84
98
|
const result = generateArcubaseProjectSDK(sdkSchemaPayload())
|
|
85
99
|
|
|
86
100
|
assert.deepEqual(result.files.map((file) => file.path).sort(), [
|
|
87
101
|
'client.ts',
|
|
88
|
-
'entities/story.ts',
|
|
89
102
|
'index.ts',
|
|
103
|
+
'ingresses/publicEntry.ts',
|
|
90
104
|
'runtime.ts',
|
|
91
105
|
'schema.ts',
|
|
92
106
|
])
|
|
93
107
|
|
|
94
108
|
const client = result.files.find((file) => file.path === 'client.ts')?.contents ?? ''
|
|
95
|
-
assert.match(client, /
|
|
96
|
-
assert.match(client, /
|
|
109
|
+
assert.match(client, /createArcubaseClient/)
|
|
110
|
+
assert.match(client, /ingress<K extends ArcubaseIngressKey>\(key: K\)/)
|
|
111
|
+
assert.match(client, /case ["']publicEntry["']/)
|
|
97
112
|
|
|
98
113
|
const runtime = result.files.find((file) => file.path === 'runtime.ts')?.contents ?? ''
|
|
99
114
|
assert.match(runtime, /accessToken/)
|
|
100
115
|
assert.match(runtime, /refreshToken/)
|
|
101
116
|
|
|
102
|
-
const
|
|
103
|
-
assert.match(
|
|
104
|
-
assert.match(
|
|
105
|
-
assert.match(
|
|
106
|
-
assert.match(
|
|
107
|
-
assert.match(
|
|
108
|
-
assert.match(
|
|
109
|
-
assert.match(
|
|
110
|
-
assert.match(
|
|
111
|
-
assert.match(
|
|
117
|
+
const ingress = result.files.find((file) => file.path === 'ingresses/publicEntry.ts')?.contents ?? ''
|
|
118
|
+
assert.match(ingress, /export type PublicEntryFields =/)
|
|
119
|
+
assert.match(ingress, /title: string/)
|
|
120
|
+
assert.match(ingress, /status\?: number \| string/)
|
|
121
|
+
assert.match(ingress, /notes\?: string/)
|
|
122
|
+
assert.match(ingress, /create\(fields: PublicEntryCreateInput\)/)
|
|
123
|
+
assert.match(ingress, /update\(rowId: string \| number, fields: PublicEntryUpdateInput\)/)
|
|
124
|
+
assert.match(ingress, /query\(params: PublicEntryQueryParams = {}\)/)
|
|
125
|
+
assert.match(ingress, /delete\(selection: PublicEntrySelection\)/)
|
|
126
|
+
assert.match(ingress, /bulkUpdate\(selection: PublicEntrySelection, fields: PublicEntryBulkUpdateField\[\]\)/)
|
|
127
|
+
assert.match(ingress, /exportRows\(options: PublicEntryExportOptions = {}\)/)
|
|
128
|
+
assert.match(ingress, /batchPrint\(selection: PublicEntrySelection/)
|
|
129
|
+
assert.match(ingress, /archive\(selection: PublicEntrySelection\)/)
|
|
130
|
+
assert.match(ingress, /tags:/)
|
|
131
|
+
assert.match(ingress, /policy_id: ingressId/)
|
|
112
132
|
|
|
113
133
|
const schema = result.files.find((file) => file.path === 'schema.ts')?.contents ?? ''
|
|
114
|
-
assert.match(schema, /"
|
|
134
|
+
assert.match(schema, /"publicEntry"/)
|
|
115
135
|
assert.match(schema, /"fieldId": 1011/)
|
|
116
136
|
assert.match(schema, /"entityId": 3883547182/)
|
|
137
|
+
assert.match(schema, /"ingressId": "8WR5Kypbw9KQK20"/)
|
|
138
|
+
assert.doesNotMatch(schema, /entity3883547182/)
|
|
117
139
|
})
|
|
118
140
|
|
|
119
141
|
test('generateArcubaseProjectSDK rejects missing field keys', () => {
|
|
120
142
|
const payload = sdkSchemaPayload()
|
|
121
|
-
payload.
|
|
143
|
+
payload.ingresses[0].entity.fields[1].key = ''
|
|
122
144
|
|
|
123
145
|
assert.throws(() => generateArcubaseProjectSDK(payload), (error: unknown) => {
|
|
124
146
|
assert.ok(error instanceof CLIError)
|
|
125
147
|
assert.equal(error.code, 'SDK_GEN_FIELD_KEY_REQUIRED')
|
|
126
|
-
assert.match(error.message,
|
|
148
|
+
assert.match(error.message, /Status/)
|
|
127
149
|
return true
|
|
128
150
|
})
|
|
129
151
|
})
|
|
130
152
|
|
|
131
153
|
test('generateArcubaseProjectSDK rejects duplicate field keys within an entity', () => {
|
|
132
154
|
const payload = sdkSchemaPayload()
|
|
133
|
-
payload.
|
|
155
|
+
payload.ingresses[0].entity.fields[1].key = 'notes'
|
|
134
156
|
|
|
135
157
|
assert.throws(() => generateArcubaseProjectSDK(payload), (error: unknown) => {
|
|
136
158
|
assert.ok(error instanceof CLIError)
|
|
137
159
|
assert.equal(error.code, 'SDK_GEN_DUPLICATE_FIELD_KEY')
|
|
138
|
-
assert.match(error.message, /
|
|
160
|
+
assert.match(error.message, /notes/)
|
|
161
|
+
return true
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('generateArcubaseProjectSDK rejects missing ingress keys', () => {
|
|
166
|
+
const payload = sdkSchemaPayload()
|
|
167
|
+
payload.ingresses[0].key = ''
|
|
168
|
+
|
|
169
|
+
assert.throws(() => generateArcubaseProjectSDK(payload), (error: unknown) => {
|
|
170
|
+
assert.ok(error instanceof CLIError)
|
|
171
|
+
assert.equal(error.code, 'SDK_GEN_INGRESS_KEY_REQUIRED')
|
|
172
|
+
assert.match(error.message, /Public entry/)
|
|
139
173
|
return true
|
|
140
174
|
})
|
|
141
175
|
})
|
|
142
176
|
|
|
177
|
+
test('generateArcubaseProjectSDK only emits methods allowed by ingress actions', () => {
|
|
178
|
+
const payload = {
|
|
179
|
+
id: 3882856425,
|
|
180
|
+
ingresses: [ingressDefinition(['view'])],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = generateArcubaseProjectSDK(payload)
|
|
184
|
+
const ingress = result.files.find((file) => file.path === 'ingresses/publicEntry.ts')?.contents ?? ''
|
|
185
|
+
assert.match(ingress, /query\(params: PublicEntryQueryParams = {}\)/)
|
|
186
|
+
assert.doesNotMatch(ingress, /create\(fields:/)
|
|
187
|
+
assert.doesNotMatch(ingress, /update\(rowId:/)
|
|
188
|
+
assert.doesNotMatch(ingress, /bulkUpdate\(/)
|
|
189
|
+
})
|
|
190
|
+
|
|
143
191
|
test('arcubase-admin dev sdk-gen fetches app schema and writes typed sdk files', async () => {
|
|
144
192
|
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-sdk-gen-'))
|
|
145
193
|
const calls: Array<{ url: string; method?: string }> = []
|
|
@@ -159,6 +207,9 @@ test('arcubase-admin dev sdk-gen fetches app schema and writes typed sdk files',
|
|
|
159
207
|
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182')) {
|
|
160
208
|
return new Response(JSON.stringify({ data: entityDetail() }), { status: 200 })
|
|
161
209
|
}
|
|
210
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182/ingress')) {
|
|
211
|
+
return new Response(JSON.stringify({ data: { list: [ingressDefinition()] } }), { status: 200 })
|
|
212
|
+
}
|
|
162
213
|
return new Response(JSON.stringify({ error: { message: `unexpected url ${url}` } }), { status: 404 })
|
|
163
214
|
},
|
|
164
215
|
)
|
|
@@ -171,30 +222,25 @@ test('arcubase-admin dev sdk-gen fetches app schema and writes typed sdk files',
|
|
|
171
222
|
assert.match(calls[1].url, /\/api\/apps\/3882856425\/entities$/)
|
|
172
223
|
assert.equal(calls[2].method, 'GET')
|
|
173
224
|
assert.match(calls[2].url, /\/api\/apps\/3882856425\/entity\/3883547182$/)
|
|
225
|
+
assert.equal(calls[3].method, 'GET')
|
|
226
|
+
assert.match(calls[3].url, /\/api\/apps\/3882856425\/entity\/3883547182\/ingress$/)
|
|
174
227
|
assert.ok(fs.existsSync(path.join(outDir, 'index.ts')))
|
|
175
228
|
assert.ok(fs.existsSync(path.join(outDir, 'client.ts')))
|
|
176
|
-
assert.ok(fs.existsSync(path.join(outDir, '
|
|
177
|
-
assert.match(fs.readFileSync(path.join(outDir, 'client.ts'), 'utf8'), /
|
|
229
|
+
assert.ok(fs.existsSync(path.join(outDir, 'ingresses/publicEntry.ts')))
|
|
230
|
+
assert.match(fs.readFileSync(path.join(outDir, 'client.ts'), 'utf8'), /createArcubaseClient/)
|
|
231
|
+
assert.notEqual(fs.existsSync(path.join(outDir, 'entities')), true)
|
|
178
232
|
})
|
|
179
233
|
|
|
180
|
-
test('arcubase-admin dev sdk-gen
|
|
234
|
+
test('arcubase-admin dev sdk-gen fails before generation when field keys are missing', async () => {
|
|
181
235
|
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-sdk-gen-'))
|
|
182
|
-
const calls: Array<{ url: string; method?: string
|
|
236
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
183
237
|
|
|
184
|
-
|
|
238
|
+
await assert.rejects(() => executeCLI(
|
|
185
239
|
'admin',
|
|
186
240
|
['dev', 'sdk-gen', '--app-id', '3882856425', '--out', outDir],
|
|
187
241
|
env as any,
|
|
188
242
|
async (url, init) => {
|
|
189
|
-
|
|
190
|
-
calls.push({
|
|
191
|
-
url: String(url),
|
|
192
|
-
method: init?.method,
|
|
193
|
-
authorization: init?.headers instanceof Headers
|
|
194
|
-
? init.headers.get('Authorization') ?? undefined
|
|
195
|
-
: (init?.headers as Record<string, string> | undefined)?.Authorization,
|
|
196
|
-
body: bodyText ? JSON.parse(bodyText) : undefined,
|
|
197
|
-
})
|
|
243
|
+
calls.push({ url: String(url), method: init?.method })
|
|
198
244
|
if (String(url).endsWith('/api/apps/3882856425')) {
|
|
199
245
|
return new Response(JSON.stringify({ data: appDetail() }), { status: 200 })
|
|
200
246
|
}
|
|
@@ -204,23 +250,50 @@ test('arcubase-admin dev sdk-gen initializes missing field keys through admin au
|
|
|
204
250
|
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182')) {
|
|
205
251
|
return new Response(JSON.stringify({ data: entityDetailWithoutFieldKeys() }), { status: 200 })
|
|
206
252
|
}
|
|
207
|
-
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182/
|
|
253
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182/ingress')) {
|
|
254
|
+
return new Response(JSON.stringify({ data: { list: [ingressDefinition()] } }), { status: 200 })
|
|
255
|
+
}
|
|
256
|
+
return new Response(JSON.stringify({ error: { message: `unexpected url ${url}` } }), { status: 404 })
|
|
257
|
+
},
|
|
258
|
+
), (error: unknown) => {
|
|
259
|
+
assert.ok(error instanceof CLIError)
|
|
260
|
+
assert.equal(error.code, 'SDK_GEN_FIELD_KEY_REQUIRED')
|
|
261
|
+
assert.match(error.message, /Records\.Title \(1006\)/)
|
|
262
|
+
return true
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
assert.equal(calls.some((call) => call.url.endsWith('/custom-keys')), false)
|
|
266
|
+
assert.equal(fs.existsSync(path.join(outDir, 'index.ts')), false)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('arcubase-admin dev sdk-gen fails before generation when ingress keys are missing', async () => {
|
|
270
|
+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-sdk-gen-'))
|
|
271
|
+
|
|
272
|
+
await assert.rejects(() => executeCLI(
|
|
273
|
+
'admin',
|
|
274
|
+
['dev', 'sdk-gen', '--app-id', '3882856425', '--out', outDir],
|
|
275
|
+
env as any,
|
|
276
|
+
async (url) => {
|
|
277
|
+
if (String(url).endsWith('/api/apps/3882856425')) {
|
|
278
|
+
return new Response(JSON.stringify({ data: appDetail() }), { status: 200 })
|
|
279
|
+
}
|
|
280
|
+
if (String(url).endsWith('/api/apps/3882856425/entities')) {
|
|
281
|
+
return new Response(JSON.stringify({ data: entityList() }), { status: 200 })
|
|
282
|
+
}
|
|
283
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182')) {
|
|
208
284
|
return new Response(JSON.stringify({ data: entityDetail() }), { status: 200 })
|
|
209
285
|
}
|
|
286
|
+
if (String(url).endsWith('/api/apps/3882856425/entity/3883547182/ingress')) {
|
|
287
|
+
return new Response(JSON.stringify({ data: { list: [{ ...ingressDefinition(), key: '' }] } }), { status: 200 })
|
|
288
|
+
}
|
|
210
289
|
return new Response(JSON.stringify({ error: { message: `unexpected url ${url}` } }), { status: 404 })
|
|
211
290
|
},
|
|
212
|
-
)
|
|
291
|
+
), (error: unknown) => {
|
|
292
|
+
assert.ok(error instanceof CLIError)
|
|
293
|
+
assert.equal(error.code, 'SDK_GEN_INGRESS_KEY_REQUIRED')
|
|
294
|
+
assert.match(error.message, /Public entry/)
|
|
295
|
+
return true
|
|
296
|
+
})
|
|
213
297
|
|
|
214
|
-
assert.equal(
|
|
215
|
-
const customKeysCall = calls.find((call) => call.url.endsWith('/api/apps/3882856425/entity/3883547182/custom-keys'))
|
|
216
|
-
assert.ok(customKeysCall)
|
|
217
|
-
assert.equal(customKeysCall.method, 'PUT')
|
|
218
|
-
assert.equal(customKeysCall.authorization, 'Bearer tok')
|
|
219
|
-
assert.deepEqual(customKeysCall.body, [
|
|
220
|
-
{ id: 1006, key: 'field1006' },
|
|
221
|
-
{ id: 1013, key: 'field1013' },
|
|
222
|
-
{ id: 1011, key: 'field1011' },
|
|
223
|
-
])
|
|
224
|
-
assert.match(fs.readFileSync(path.join(outDir, 'entities/story.ts'), 'utf8'), /field1011: string/)
|
|
225
|
-
assert.deepEqual(result.data.initializedFieldKeys, [{ entityId: 3883547182, fields: ['field1006', 'field1013', 'field1011'] }])
|
|
298
|
+
assert.equal(fs.existsSync(path.join(outDir, 'index.ts')), false)
|
|
226
299
|
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -36,8 +36,9 @@ test('admin dev sdk-gen help documents typed project sdk generation', async () =
|
|
|
36
36
|
assert.match(commandHelp.text, /arcubase-admin dev sdk-gen/)
|
|
37
37
|
assert.match(commandHelp.text, /--app-id/)
|
|
38
38
|
assert.match(commandHelp.text, /--out/)
|
|
39
|
+
assert.match(commandHelp.text, /ingress\.key/)
|
|
39
40
|
assert.match(commandHelp.text, /field\.key/)
|
|
40
|
-
assert.match(commandHelp.text, /
|
|
41
|
+
assert.match(commandHelp.text, /createArcubaseClient\(\{ baseURL, accessToken, refreshToken \}\)\.ingress\("<IngressKey>"\)/)
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
test('user root help exposes reset surface nouns only', async () => {
|