@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.
@@ -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
- ' - missing field.key values are initialized through admin auth as field<id>',
299
- ' - existing field.key values must be stable unique TypeScript identifiers',
300
- ' - entity property names use entity.key when available, otherwise a stable entity<id> fallback',
301
- ' - generated code accepts createArcubaseSdk({ baseURL, accessToken, refreshToken })',
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 SDKGenInitializedFieldKeys = {
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 collectExistingSDKGenFieldKeys(entity: Record<string, any>): Set<string> {
2060
- const keys = new Set<string>()
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
- keys.add(field.key.trim())
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 nextSDKGenFallbackFieldKey(fieldId: number, usedKeys: Set<string>): string {
2070
- const base = `field${fieldId}`
2071
- if (!usedKeys.has(base)) {
2072
- usedKeys.add(base)
2073
- return base
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
- let index = 2
2076
- while (usedKeys.has(`${base}_${index}`)) {
2077
- index++
2108
+ if (typeof ingress.hashId === 'string' && ingress.hashId.trim()) {
2109
+ return ingress.hashId.trim()
2078
2110
  }
2079
- const key = `${base}_${index}`
2080
- usedKeys.add(key)
2081
- return key
2111
+ return ''
2082
2112
  }
2083
2113
 
2084
- function initializeMissingSDKGenFieldKeys(entity: Record<string, any>): Array<{ id: number; key: string }> {
2085
- const usedKeys = collectExistingSDKGenFieldKeys(entity)
2086
- const updates: Array<{ id: number; key: string }> = []
2087
- walkSDKGenFields(entity.fields, (field) => {
2088
- if (typeof field.id !== 'number') {
2089
- return
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 (typeof field.key === 'string' && field.key.trim() !== '') {
2092
- field.key = field.key.trim()
2093
- return
2134
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key)) {
2135
+ invalidKey.push(key)
2094
2136
  }
2095
- const key = nextSDKGenFallbackFieldKey(field.id, usedKeys)
2096
- field.key = key
2097
- updates.push({ id: field.id, key })
2098
- })
2099
- return updates
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 entities = []
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
- const customKeyUpdates = initializeMissingSDKGenFieldKeys(entityPayload)
2143
- if (customKeyUpdates.length > 0) {
2144
- const customKeysEndpoint = `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(entityRef.id)}/custom-keys`
2145
- await requestJSON(runtimeEnv, 'PUT', customKeysEndpoint, customKeyUpdates, fetchImpl)
2146
- initializedFieldKeys.push({
2147
- entityId: typeof entityPayload.id === 'number' ? entityPayload.id : Number(entityRef.id),
2148
- fields: customKeyUpdates.map((item) => item.key),
2149
- })
2150
- }
2151
- entities.push(entityPayload)
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
- entities,
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: '时代', key: 'era', type: 'select', required: false, options: { options: { items: [{ key: 1, value: '现代' }] } } },
59
- { id: 1013, label: '你刚刚', key: 'recentEvent', type: 'text', required: false, options: {} },
60
- { id: 1011, label: '故事', key: 'story', type: 'textarea', required: true, options: {} },
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
- ...appDetailWithEntityList(),
74
- entities: [
75
- {
76
- ...entityDetail(),
77
- key: 'story',
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 key based entity sdk', () => {
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, /createArcubaseSdk/)
96
- assert.match(client, /story: createStorySDK\(runtime\)/)
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 story = result.files.find((file) => file.path === 'entities/story.ts')?.contents ?? ''
103
- assert.match(story, /export type StoryFields =/)
104
- assert.match(story, /era\?: number \| string/)
105
- assert.match(story, /recentEvent\?: string/)
106
- assert.match(story, /story: string/)
107
- assert.match(story, /create\(fields: StoryCreateInput\)/)
108
- assert.match(story, /update\(rowId: string \| number, fields: StoryUpdateInput\)/)
109
- assert.match(story, /toArcubaseFields\(fields\)/)
110
- assert.match(story, /changed_fields: Object.keys\(data\)\.map\(\(fieldId\) => Number\(fieldId\)\)/)
111
- assert.match(story, /\/entity\/3882856425\/\$\{entityId\}\/insert/)
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, /"story"/)
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.entities[0].fields[1].key = ''
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.entities[0].fields[1].key = 'story'
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, /story/)
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, 'entities/story.ts')))
177
- assert.match(fs.readFileSync(path.join(outDir, 'client.ts'), 'utf8'), /createArcubaseSdk/)
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 initializes missing field keys through admin auth before generation', async () => {
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; authorization?: string; body?: any }> = []
236
+ const calls: Array<{ url: string; method?: string }> = []
183
237
 
184
- const result = await executeCLI(
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
- const bodyText = typeof init?.body === 'string' ? init.body : undefined
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/custom-keys')) {
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(result.kind, 'result')
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
  })
@@ -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, /createArcubaseSdk\(\{ baseURL, accessToken, refreshToken \}\)/)
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 () => {
@@ -46,6 +46,7 @@ test('AppIngressCreateReqVO accepts nested ConditionSet with field EntityProp co
46
46
  assert.ok(schema)
47
47
 
48
48
  const result = schema.safeParse({
49
+ key: 'salesOwnOrWestRegion',
49
50
  name: 'Sales own or west region',
50
51
  enabled: true,
51
52
  type: 'form',