fix(field): enforce select-visibility guard on the V2 convert path + repair inkreach-cli field routes

- FieldOpenApiV2Service.convertField now calls the shared V1 validator
  (assertSelectVisibilityConfigValid) before executing the update command.
  Previously only the V1 path validated self-reference / dependency cycles,
  so invalid configs could be persisted through V2 (verified live: a
  self-referencing choice was accepted with 200).
- V1 assertSelectVisibilityConfigValid private -> public (shared guard).
- Unit tests: 3 new cases in field-open-api-v2.service.spec.ts (guard
  invoked with exact args; rejects before executing when validator throws;
  called with raw type/options for non-select conversions). 63/63 pass;
  tsc build config 0 errors.
- inkreach-cli: field get/update/delete targeted non-existent /api/field/:id
  routes; now use /api/table/:tableId/field/:fieldId. update --options goes
  through the convert API (the PATCH update API strips options by design).
  New: convertField + getVisibleChoices client methods, field visible-choices
  command (evaluates choice visibility for a record).
This commit is contained in:
yeuimu
2026-08-16 05:31:05 +00:00
parent 4dc65c2a0c
commit 10b52db8b2
4 changed files with 146 additions and 43 deletions
+25 -6
View File
@@ -77,17 +77,36 @@ export class Client {
listFields(tableId: string) {
return this.call<unknown[]>('GET', `/api/table/${tableId}/field`);
}
getField(fieldId: string) {
return this.call<unknown>('GET', `/api/field/${fieldId}`);
getField(tableId: string, fieldId: string) {
return this.call<unknown>('GET', `/api/table/${tableId}/field/${fieldId}`);
}
createField(tableId: string, body: unknown) {
return this.call<unknown>('POST', `/api/table/${tableId}/field`, body);
}
updateField(fieldId: string, body: unknown) {
return this.call<unknown>('PUT', `/api/field/${fieldId}`, body);
/** Rename/describe. NOTE: the update API accepts name/description only —
* options (incl. select choice visibilityFilter) must go through convertField. */
updateField(tableId: string, fieldId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/table/${tableId}/field/${fieldId}`, body);
}
deleteField(fieldId: string) {
return this.call<{ id: string }>('DELETE', `/api/field/${fieldId}`);
/** PUT convert: the ONLY API path that persists field options (choices,
* visibilityFilter dependency config, etc.). Requires the field's current type. */
convertField(tableId: string, fieldId: string, body: unknown) {
return this.call<unknown>(
'PUT',
`/api/table/${tableId}/field/${fieldId}/convert`,
body
);
}
deleteField(tableId: string, fieldId: string) {
return this.call<{ id: string }>('DELETE', `/api/table/${tableId}/field/${fieldId}`);
}
/** Evaluate select-choice visibility dependency for one record+field.
* GET /api/table/:tableId/select-visibility/:recordId/:fieldId */
getVisibleChoices(tableId: string, recordId: string, fieldId: string) {
return this.call<{ visibleChoices: Array<{ id: string; name: string; color: string }> }>(
'GET',
`/api/table/${tableId}/select-visibility/${recordId}/${fieldId}`
);
}
// ---- record ----