Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/view-ref-build-lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@objectstack/spec": minor
"@objectstack/cli": minor
---

Add a build-time view-reference lint that fails `os compile` on a broken form-view reference, and surfaces the previously-silent `_2` rename collision as a warning (#2554).

`expandViewContainer` gains a behaviour-preserving companion `expandViewContainerWithDiagnostics` that also reports every `<object>.<key>` name collision. List and form views share one namespace during expansion, and the default `list` implicitly claims `<object>.default`; a colliding key was previously renamed to `<object>.<key>_2` **silently**, so references (form action `target`s, navigation `viewName`s) resolved to the *other* view.

The new `lint-view-refs` build lint consumes those diagnostics with a broken/fragile severity split, tuned so an upgrade does NOT break existing apps that merely have a colliding key:

- **view-ref-form-target-kind** — ERROR (fails the build): a `type:'form'` action whose `target` resolves to an existing LIST view — the concrete #2554 breakage (a blank form, a silently no-op submit). High-confidence, so it fails.
- **view-key-collision** — WARNING: a key silently renamed on collision. Fragile, not broken — it breaks something only if the requested name is referenced — so it warns.
- **view-ref-form-target-missing** — WARNING: a form target resolving to no view; probably a typo, but possibly a view the lint failed to collect, so it warns rather than risk a false-positive build failure.

This shifts objectui's runtime `viewKind` guard left to compile time: the author — very often an AI generating templates — discovers the mistake on `os compile` instead of when an end user clicks. It mirrors the existing broken/fragile two-level authoring lints (flow-patterns, autonumber, liveness). `expandViewContainer`'s runtime behaviour is unchanged; the fix is diagnostics-only plus the build gate.
7 changes: 4 additions & 3 deletions content/docs/references/ai/solution-blueprint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const result = BlueprintApp.parse(data);
| **label** | `string` | optional | Human-readable singular label |
| **description** | `string` | optional | What this object represents |
| **fields** | `Object[]` | ✅ | Fields to create on the object |
| **nameField** | `string` | optional | The record title field — which field holds the human-readable name shown on cards, lookup chips, breadcrumbs and search (ADR-0079). Set it to the object's text label field (e.g. "product_name"). For a numbered entity (invoice/ticket), set it to a formula field that composes number + name (e.g. "`{order_no}` · `{customer}`"). Omitting it lets the platform auto-pick a text field, but declaring it is strongly preferred. |


---
Expand All @@ -135,9 +136,9 @@ const result = BlueprintApp.parse(data);
| **object** | `string` | ✅ | Object this view displays (snake_case) |
| **name** | `string` | ✅ | View machine name (snake_case) |
| **label** | `string` | optional | Human-readable view label |
| **type** | `Enum<'list' \| 'form' \| 'kanban' \| 'calendar'>` | ✅ | View kind |
| **columns** | `string[]` | optional | Field names shown as columns (in order) |
| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. |
| **type** | `Enum<'list' \| 'form' \| 'kanban' \| 'calendar' \| 'gallery' \| 'gantt'>` | ✅ | View kind. Pick the surface that fits the data: "gallery" for a visual card/cover browse when the user asks for a 画廊/相册/卡片墙/封面/海报/图集 (a gallery / card wall / cover / poster grid) or the object has an image/avatar/file field worth showing as a card cover; "gantt" for a 甘特图/时间线/排期 (timeline / schedule) when the object has BOTH a start and an end date field; "kanban" for a board grouped by a status/select field; "calendar" for a single-date schedule; "form" for a record editor; else "list". |
| **columns** | `string[]` | optional | Field names shown as columns (in order). For a gallery, INCLUDE the image/avatar/file field (it becomes the card cover); for a gantt, INCLUDE the start date column before the end date column. |
| **groupBy** | `string` | optional | REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows). |


---
Expand Down
271 changes: 269 additions & 2 deletions content/docs/references/api/export.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Base path: /api/v1/data/\{object\}/export
## TypeScript Usage

```typescript
import { CreateExportJobRequest, CreateExportJobResponse, DeduplicationStrategy, ExportFormat, ExportImportTemplate, ExportJobProgress, ExportJobStatus, ExportJobSummary, FieldMappingEntry, GetExportJobDownloadRequest, GetExportJobDownloadResponse, ImportValidationConfig, ImportValidationMode, ImportValidationResult, ListExportJobsRequest, ListExportJobsResponse, ScheduleExportResponse } from '@objectstack/spec/api';
import type { CreateExportJobRequest, CreateExportJobResponse, DeduplicationStrategy, ExportFormat, ExportImportTemplate, ExportJobProgress, ExportJobStatus, ExportJobSummary, FieldMappingEntry, GetExportJobDownloadRequest, GetExportJobDownloadResponse, ImportValidationConfig, ImportValidationMode, ImportValidationResult, ListExportJobsRequest, ListExportJobsResponse, ScheduleExportResponse } from '@objectstack/spec/api';
import { CreateExportJobRequest, CreateExportJobResponse, CreateImportJobRequest, CreateImportJobResponse, DeduplicationStrategy, ExportFormat, ExportImportTemplate, ExportJobProgress, ExportJobStatus, ExportJobSummary, FieldMappingEntry, GetExportJobDownloadRequest, GetExportJobDownloadResponse, ImportJobProgress, ImportJobResults, ImportJobStatus, ImportJobSummary, ImportMapping, ImportRequest, ImportResponse, ImportRowResult, ImportValidationConfig, ImportValidationMode, ImportValidationResult, ImportWriteMode, ListExportJobsRequest, ListExportJobsResponse, ListImportJobsRequest, ListImportJobsResponse, ScheduleExportResponse, UndoImportJobResponse } from '@objectstack/spec/api';
import type { CreateExportJobRequest, CreateExportJobResponse, CreateImportJobRequest, CreateImportJobResponse, DeduplicationStrategy, ExportFormat, ExportImportTemplate, ExportJobProgress, ExportJobStatus, ExportJobSummary, FieldMappingEntry, GetExportJobDownloadRequest, GetExportJobDownloadResponse, ImportJobProgress, ImportJobResults, ImportJobStatus, ImportJobSummary, ImportMapping, ImportRequest, ImportResponse, ImportRowResult, ImportValidationConfig, ImportValidationMode, ImportValidationResult, ImportWriteMode, ListExportJobsRequest, ListExportJobsResponse, ListImportJobsRequest, ListImportJobsResponse, ScheduleExportResponse, UndoImportJobResponse } from '@objectstack/spec/api';

// Validate data
const result = CreateExportJobRequest.parse(data);
Expand Down Expand Up @@ -64,6 +64,45 @@ const result = CreateExportJobRequest.parse(data);
| **data** | `Object` | ✅ | |


---

## CreateImportJobRequest

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **format** | `Enum<'csv' \| 'json' \| 'xlsx'>` | optional | Payload shape: csv text, a rows[] array, or a base64 xlsx (inferred when omitted) |
| **csv** | `string` | optional | CSV text (when format = csv) |
| **rows** | `Record<string, any>[]` | optional | Row objects (when format = json) |
| **xlsxBase64** | `string` | optional | Base64-encoded .xlsx workbook bytes (when format = xlsx); parsed server-side |
| **sheet** | `string \| integer` | optional | Worksheet name or 1-based index to read (xlsx; defaults to the first sheet) |
| **mapping** | `Record<string, string> \| Object[]` | optional | Source column → target field mapping |
| **dryRun** | `boolean` | ✅ | Validate + coerce every row without persisting |
| **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | insert / update / upsert semantics |
| **matchFields** | `string[]` | optional | Fields that identify an existing record (required for update/upsert) |
| **runAutomations** | `boolean` | ✅ | Fire triggers/hooks for each imported row (off by default for bulk) |
| **trimWhitespace** | `boolean` | ✅ | Trim leading/trailing whitespace from string cells |
| **nullValues** | `string[]` | optional | Strings treated as null/blank (besides empty string) |
| **createMissingOptions** | `boolean` | ✅ | Keep unmatched select values instead of failing the row |
| **skipBlankMatchKey** | `boolean` | ✅ | Skip rows whose matchFields are blank (default: upsert creates them, update skips them) |


---

## CreateImportJobResponse

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **jobId** | `string` | ✅ | Import job id — poll progress/results with this |
| **object** | `string` | ✅ | Target object name |
| **status** | `Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>` | ✅ | Initial job status (usually "pending") |
| **total** | `integer` | ✅ | Rows accepted for processing |
| **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) |


---

## DeduplicationStrategy
Expand Down Expand Up @@ -198,6 +237,182 @@ const result = CreateExportJobRequest.parse(data);
| **data** | `Object` | ✅ | |


---

## ImportJobProgress

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **jobId** | `string` | ✅ | Import job id |
| **object** | `string` | ✅ | Target object name |
| **status** | `Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>` | ✅ | Current job status |
| **dryRun** | `boolean` | ✅ | Whether this is a validate-only pass |
| **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | Write mode used |
| **total** | `integer` | ✅ | Total rows to process |
| **processed** | `integer` | ✅ | Rows processed so far |
| **created** | `integer` | ✅ | Rows that created a new record |
| **updated** | `integer` | ✅ | Rows that updated an existing record |
| **skipped** | `integer` | ✅ | Rows skipped |
| **errors** | `integer` | ✅ | Rows that failed |
| **percentComplete** | `number` | ✅ | processed / total as a percentage |
| **undoable** | `boolean` | ✅ | Whether this job can still be logically rolled back (undo log captured, terminal state, not yet reverted) |
| **revertedAt** | `string` | optional | When the job was undone / rolled back (ISO 8601) |
| **error** | `string` | optional | Fatal error message (when status = failed) |
| **startedAt** | `string` | optional | Processing start timestamp (ISO 8601) |
| **completedAt** | `string` | optional | Completion timestamp (ISO 8601) |
| **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) |


---

## ImportJobResults

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **jobId** | `string` | ✅ | Import job id |
| **object** | `string` | ✅ | Target object name |
| **status** | `Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>` | ✅ | Current job status |
| **dryRun** | `boolean` | ✅ | Whether this is a validate-only pass |
| **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | Write mode used |
| **total** | `integer` | ✅ | Total rows to process |
| **processed** | `integer` | ✅ | Rows processed so far |
| **created** | `integer` | ✅ | Rows that created a new record |
| **updated** | `integer` | ✅ | Rows that updated an existing record |
| **skipped** | `integer` | ✅ | Rows skipped |
| **errors** | `integer` | ✅ | Rows that failed |
| **percentComplete** | `number` | ✅ | processed / total as a percentage |
| **undoable** | `boolean` | ✅ | Whether this job can still be logically rolled back (undo log captured, terminal state, not yet reverted) |
| **revertedAt** | `string` | optional | When the job was undone / rolled back (ISO 8601) |
| **error** | `string` | optional | Fatal error message (when status = failed) |
| **startedAt** | `string` | optional | Processing start timestamp (ISO 8601) |
| **completedAt** | `string` | optional | Completion timestamp (ISO 8601) |
| **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) |
| **results** | `Object[]` | ✅ | Capped sample of per-row outcomes (failures first) |
| **resultsTruncated** | `boolean` | ✅ | Whether `results` is a capped sample of a larger set |


---

## ImportJobStatus

### Allowed Values

* `pending`
* `running`
* `succeeded`
* `failed`
* `cancelled`


---

## ImportJobSummary

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **jobId** | `string` | ✅ | Import job id |
| **object** | `string` | ✅ | Target object name |
| **status** | `Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>` | ✅ | Job status |
| **total** | `integer` | ✅ | Total rows |
| **processed** | `integer` | ✅ | Rows processed |
| **created** | `integer` | ✅ | Rows created |
| **updated** | `integer` | ✅ | Rows updated |
| **skipped** | `integer` | ✅ | Rows skipped |
| **errors** | `integer` | ✅ | Rows failed |
| **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) |
| **completedAt** | `string` | optional | Completion timestamp (ISO 8601) |
| **undoable** | `boolean` | ✅ | Whether this job can still be logically rolled back |
| **revertedAt** | `string` | optional | When the job was undone / rolled back (ISO 8601) |


---

## ImportMapping

### Union Options

This schema accepts one of the following structures:

#### Option 1

Type: `Record<string, string>`

---

#### Option 2

Type: `Object[]`

---


---

## ImportRequest

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **format** | `Enum<'csv' \| 'json' \| 'xlsx'>` | optional | Payload shape: csv text, a rows[] array, or a base64 xlsx (inferred when omitted) |
| **csv** | `string` | optional | CSV text (when format = csv) |
| **rows** | `Record<string, any>[]` | optional | Row objects (when format = json) |
| **xlsxBase64** | `string` | optional | Base64-encoded .xlsx workbook bytes (when format = xlsx); parsed server-side |
| **sheet** | `string \| integer` | optional | Worksheet name or 1-based index to read (xlsx; defaults to the first sheet) |
| **mapping** | `Record<string, string> \| Object[]` | optional | Source column → target field mapping |
| **dryRun** | `boolean` | ✅ | Validate + coerce every row without persisting |
| **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | insert / update / upsert semantics |
| **matchFields** | `string[]` | optional | Fields that identify an existing record (required for update/upsert) |
| **runAutomations** | `boolean` | ✅ | Fire triggers/hooks for each imported row (off by default for bulk) |
| **trimWhitespace** | `boolean` | ✅ | Trim leading/trailing whitespace from string cells |
| **nullValues** | `string[]` | optional | Strings treated as null/blank (besides empty string) |
| **createMissingOptions** | `boolean` | ✅ | Keep unmatched select values instead of failing the row |
| **skipBlankMatchKey** | `boolean` | ✅ | Skip rows whose matchFields are blank (default: upsert creates them, update skips them) |


---

## ImportResponse

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **object** | `string` | ✅ | Target object name |
| **dryRun** | `boolean` | ✅ | Whether this was a validate-only pass |
| **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | Write mode used |
| **total** | `integer` | ✅ | Rows processed |
| **ok** | `integer` | ✅ | Rows that succeeded |
| **errors** | `integer` | ✅ | Rows that failed |
| **created** | `integer` | ✅ | Rows that created a new record |
| **updated** | `integer` | ✅ | Rows that updated an existing record |
| **skipped** | `integer` | ✅ | Rows skipped (no match in update mode, etc.) |
| **results** | `Object[]` | ✅ | Per-row outcomes |


---

## ImportRowResult

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **row** | `integer` | ✅ | 1-based row number in the source data |
| **ok** | `boolean` | ✅ | Whether the row succeeded |
| **action** | `Enum<'created' \| 'updated' \| 'skipped' \| 'failed'>` | ✅ | What happened to the row |
| **id** | `string` | optional | Record id (created/updated rows) |
| **field** | `string` | optional | Field that caused a coercion/validation error |
| **code** | `string` | optional | Error code (failed rows) |
| **error** | `string` | optional | Human-readable error message (failed rows) |


---

## ImportValidationConfig
Expand Down Expand Up @@ -239,6 +454,17 @@ const result = CreateExportJobRequest.parse(data);
| **data** | `Object` | ✅ | |


---

## ImportWriteMode

### Allowed Values

* `insert`
* `update`
* `upsert`


---

## ListExportJobsRequest
Expand Down Expand Up @@ -267,6 +493,31 @@ const result = CreateExportJobRequest.parse(data);
| **data** | `Object` | ✅ | |


---

## ListImportJobsRequest

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **object** | `string` | optional | Filter to one target object |
| **status** | `Enum<'pending' \| 'running' \| 'succeeded' \| 'failed' \| 'cancelled'>` | optional | Filter by job status |
| **limit** | `integer` | ✅ | Max rows to return |
| **offset** | `integer` | ✅ | Pagination offset |


---

## ListImportJobsResponse

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **jobs** | `Object[]` | ✅ | Import jobs, newest first |


---

## ScheduleExportResponse
Expand All @@ -283,3 +534,19 @@ const result = CreateExportJobRequest.parse(data);

---

## UndoImportJobResponse

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **success** | `boolean` | ✅ | Whether the undo completed |
| **jobId** | `string` | ✅ | Import job id |
| **object** | `string` | ✅ | Target object name |
| **deleted** | `integer` | ✅ | Created records deleted |
| **restored** | `integer` | ✅ | Updated records restored to pre-import values |
| **failed** | `integer` | ✅ | Reversal operations that failed |


---

Loading