From 6e64a0574f2603eefc97e17bf9a874196afb13d0 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:38:05 +0800 Subject: [PATCH 1/3] feat(spec): defineX factories + XInput aliases for the 16 remaining writable domains (#2035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds one type-safe authoring entry per writable domain — defineDatasource, defineConnector, definePolicy, defineSharingRule, defineRole, definePermissionSet, defineEmailTemplateDefinition, defineReport, defineWebhook, defineObjectExtension, defineCube, defineMapping, defineTheme, defineTranslationBundle, definePage, defineAction — mirroring the 19 existing factories (XSchema.parse(z.input)). Each factory is a *value* export: a broken import hard-errors instead of silently degrading to `any` (the #2023 failure mode), and authoring errors surface at .parse() time with field-level messages. Also backfills the six missing input aliases: PolicyInput, CubeInput, MappingInput, ThemeInput, TranslationBundleInput, PageInput. Purely additive — no existing exports change. Co-Authored-By: Claude Opus 4.8 --- .changeset/spec-define-x-factories.md | 22 +++++++++++++++++++ packages/spec/src/automation/webhook.zod.ts | 9 ++++++++ packages/spec/src/data/analytics.zod.ts | 11 ++++++++++ packages/spec/src/data/datasource.zod.ts | 9 ++++++++ packages/spec/src/data/mapping.zod.ts | 11 ++++++++++ packages/spec/src/data/object.zod.ts | 9 ++++++++ packages/spec/src/identity/role.zod.ts | 9 ++++++++ packages/spec/src/index.ts | 21 ++++++++++++++++++ .../spec/src/integration/connector.zod.ts | 9 ++++++++ packages/spec/src/security/permission.zod.ts | 9 ++++++++ packages/spec/src/security/policy.zod.ts | 11 ++++++++++ packages/spec/src/security/sharing.zod.ts | 9 ++++++++ .../spec/src/system/email-template.zod.ts | 9 ++++++++ packages/spec/src/system/translation.zod.ts | 11 ++++++++++ packages/spec/src/ui/action.zod.ts | 9 ++++++++ packages/spec/src/ui/page.zod.ts | 11 ++++++++++ packages/spec/src/ui/report.zod.ts | 9 ++++++++ packages/spec/src/ui/theme.zod.ts | 11 ++++++++++ 18 files changed, 199 insertions(+) create mode 100644 .changeset/spec-define-x-factories.md diff --git a/.changeset/spec-define-x-factories.md b/.changeset/spec-define-x-factories.md new file mode 100644 index 0000000000..febe9cab57 --- /dev/null +++ b/.changeset/spec-define-x-factories.md @@ -0,0 +1,22 @@ +--- +"@objectstack/spec": minor +--- + +spec: add `defineX` factories for the remaining 16 writable domains and the 6 +missing `XInput` aliases — one consistent, type-safe authoring entry per domain +(#2035). + +New factories: `defineDatasource`, `defineConnector`, `definePolicy`, +`defineSharingRule`, `defineRole`, `definePermissionSet`, +`defineEmailTemplateDefinition`, `defineReport`, `defineWebhook`, +`defineObjectExtension`, `defineCube`, `defineMapping`, `defineTheme`, +`defineTranslationBundle`, `definePage`, `defineAction`. Each mirrors the 19 +existing factories (`XSchema.parse(z.input<…>)`): input-shape ergonomics + +authoring-time validation. Because a factory is a *value* import, a broken +import hard-errors instead of silently degrading to `any` (the #2023 failure +mode), and errors surface at `.parse()` time with field-level messages. + +Also adds the previously-missing input aliases `PolicyInput`, `CubeInput`, +`MappingInput`, `ThemeInput`, `TranslationBundleInput`, `PageInput`. + +Purely additive: no existing exports change. diff --git a/packages/spec/src/automation/webhook.zod.ts b/packages/spec/src/automation/webhook.zod.ts index d7681ac398..2b773180b3 100644 --- a/packages/spec/src/automation/webhook.zod.ts +++ b/packages/spec/src/automation/webhook.zod.ts @@ -144,3 +144,12 @@ export type Webhook = z.infer; /** Authoring input for {@link Webhook} — defaulted fields are optional. */ export type WebhookInput = z.input; export type WebhookReceiver = z.infer; + +/** + * Type-safe factory for an outbound webhook. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Webhook` literal. + */ +export function defineWebhook(config: z.input): Webhook { + return WebhookSchema.parse(config); +} diff --git a/packages/spec/src/data/analytics.zod.ts b/packages/spec/src/data/analytics.zod.ts index 1dbf2faf4d..8be3b647ef 100644 --- a/packages/spec/src/data/analytics.zod.ts +++ b/packages/spec/src/data/analytics.zod.ts @@ -174,4 +174,15 @@ export type Metric = z.infer; export type Dimension = z.infer; export type CubeJoin = z.infer; export type Cube = z.infer; +/** Authoring input for {@link Cube} — defaulted fields are optional. */ +export type CubeInput = z.input; + +/** + * Type-safe factory for an analytics semantic-layer cube. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Cube` literal. + */ +export function defineCube(config: z.input): Cube { + return CubeSchema.parse(config); +} export type AnalyticsQuery = z.infer; diff --git a/packages/spec/src/data/datasource.zod.ts b/packages/spec/src/data/datasource.zod.ts index 899127c886..2ef55ea3f6 100644 --- a/packages/spec/src/data/datasource.zod.ts +++ b/packages/spec/src/data/datasource.zod.ts @@ -260,3 +260,12 @@ export type Datasource = z.infer; /** Authoring input for {@link Datasource} — defaulted fields are optional. */ export type DatasourceInput = z.input; export type DatasourceCapabilitiesType = z.infer; + +/** + * Type-safe factory for an external data connection (datasource). Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Datasource` literal. + */ +export function defineDatasource(config: z.input): Datasource { + return DatasourceSchema.parse(config); +} diff --git a/packages/spec/src/data/mapping.zod.ts b/packages/spec/src/data/mapping.zod.ts index aee7521bd6..5610083af8 100644 --- a/packages/spec/src/data/mapping.zod.ts +++ b/packages/spec/src/data/mapping.zod.ts @@ -92,4 +92,15 @@ export const MappingSchema = lazySchema(() => z.object({ })); export type Mapping = z.infer; +/** Authoring input for {@link Mapping} — defaulted fields are optional. */ +export type MappingInput = z.input; + +/** + * Type-safe factory for a data import/export mapping. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Mapping` literal. + */ +export function defineMapping(config: z.input): Mapping { + return MappingSchema.parse(config); +} export type FieldMapping = z.infer; diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 8a9feb5f61..3eddbd8063 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -1011,3 +1011,12 @@ export const ObjectExtensionSchema = lazySchema(() => z.object({ export type ObjectExtension = z.infer; /** Authoring input for {@link ObjectExtension} — defaulted fields are optional. */ export type ObjectExtensionInput = z.input; + +/** + * Type-safe factory for an extension to an object owned by another package. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: ObjectExtension` literal. + */ +export function defineObjectExtension(config: z.input): ObjectExtension { + return ObjectExtensionSchema.parse(config); +} diff --git a/packages/spec/src/identity/role.zod.ts b/packages/spec/src/identity/role.zod.ts index 2d481eb609..55605efde0 100644 --- a/packages/spec/src/identity/role.zod.ts +++ b/packages/spec/src/identity/role.zod.ts @@ -45,3 +45,12 @@ export const RoleSchema = lazySchema(() => z.object({ export type Role = z.infer; /** Authoring input for {@link Role} — defaulted fields are optional. */ export type RoleInput = z.input; + +/** + * Type-safe factory for a role in the role hierarchy. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Role` literal. + */ +export function defineRole(config: z.input): Role { + return RoleSchema.parse(config); +} diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index a6969ebfca..17c153b41f 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -80,6 +80,27 @@ export { defineBook } from './system/book.zod'; export { defineAgent } from './ai/agent.zod'; export { defineTool } from './ai/tool.zod'; export { defineSkill } from './ai/skill.zod'; + +// DX factories for the remaining authoring domains (issue #2035) — one type-safe +// entry per writable domain, mirroring the 19 factories above. `defineX` is a +// *value* import: a broken import hard-errors instead of silently degrading to +// `any` (the #2023 failure mode). Input-shape config + runtime `.parse()`. +export { defineDatasource } from './data/datasource.zod'; +export { defineConnector } from './integration/connector.zod'; +export { definePolicy } from './security/policy.zod'; +export { defineSharingRule } from './security/sharing.zod'; +export { defineRole } from './identity/role.zod'; +export { definePermissionSet } from './security/permission.zod'; +export { defineEmailTemplateDefinition } from './system/email-template.zod'; +export { defineReport } from './ui/report.zod'; +export { defineWebhook } from './automation/webhook.zod'; +export { defineObjectExtension } from './data/object.zod'; +export { defineCube } from './data/analytics.zod'; +export { defineMapping } from './data/mapping.zod'; +export { defineTheme } from './ui/theme.zod'; +export { defineTranslationBundle } from './system/translation.zod'; +export { definePage } from './ui/page.zod'; +export { defineAction } from './ui/action.zod'; export type { Agent } from './ai/agent.zod'; export type { Tool } from './ai/tool.zod'; export type { Skill } from './ai/skill.zod'; diff --git a/packages/spec/src/integration/connector.zod.ts b/packages/spec/src/integration/connector.zod.ts index 8a33979d1f..630710b420 100644 --- a/packages/spec/src/integration/connector.zod.ts +++ b/packages/spec/src/integration/connector.zod.ts @@ -630,3 +630,12 @@ export const ConnectorSchema = lazySchema(() => z.object({ export type Connector = z.infer; /** Authoring input for {@link Connector} — defaulted fields are optional. */ export type ConnectorInput = z.input; + +/** + * Type-safe factory for an external-system connector. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Connector` literal. + */ +export function defineConnector(config: z.input): Connector { + return ConnectorSchema.parse(config); +} diff --git a/packages/spec/src/security/permission.zod.ts b/packages/spec/src/security/permission.zod.ts index fbd6137fbd..97fa5f2ed4 100644 --- a/packages/spec/src/security/permission.zod.ts +++ b/packages/spec/src/security/permission.zod.ts @@ -196,3 +196,12 @@ export type PermissionSet = z.infer; export type PermissionSetInput = z.input; export type ObjectPermission = z.infer; export type FieldPermission = z.infer; + +/** + * Type-safe factory for a permission set / profile. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: PermissionSet` literal. + */ +export function definePermissionSet(config: z.input): PermissionSet { + return PermissionSetSchema.parse(config); +} diff --git a/packages/spec/src/security/policy.zod.ts b/packages/spec/src/security/policy.zod.ts index 1ac0e1033a..565962d067 100644 --- a/packages/spec/src/security/policy.zod.ts +++ b/packages/spec/src/security/policy.zod.ts @@ -77,3 +77,14 @@ export const PolicySchema = lazySchema(() => z.object({ })); export type Policy = z.infer; +/** Authoring input for {@link Policy} — defaulted fields are optional. */ +export type PolicyInput = z.input; + +/** + * Type-safe factory for a security / compliance policy. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Policy` literal. + */ +export function definePolicy(config: z.input): Policy { + return PolicySchema.parse(config); +} diff --git a/packages/spec/src/security/sharing.zod.ts b/packages/spec/src/security/sharing.zod.ts index 291158f026..f85b642072 100644 --- a/packages/spec/src/security/sharing.zod.ts +++ b/packages/spec/src/security/sharing.zod.ts @@ -112,3 +112,12 @@ export type SharingRule = z.infer; export type SharingRuleInput = z.input; export type CriteriaSharingRule = z.infer; export type OwnerSharingRule = z.infer; + +/** + * Type-safe factory for a record sharing rule. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: SharingRule` literal. + */ +export function defineSharingRule(config: z.input): SharingRule { + return SharingRuleSchema.parse(config); +} diff --git a/packages/spec/src/system/email-template.zod.ts b/packages/spec/src/system/email-template.zod.ts index 65dd0fe183..e0fcffb430 100644 --- a/packages/spec/src/system/email-template.zod.ts +++ b/packages/spec/src/system/email-template.zod.ts @@ -135,3 +135,12 @@ function EmailAddressInlineSchema() { export type EmailTemplateDefinition = z.infer; /** Authoring input for {@link EmailTemplateDefinition} — defaulted fields are optional. */ export type EmailTemplateDefinitionInput = z.input; + +/** + * Type-safe factory for an email template. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: EmailTemplateDefinition` literal. + */ +export function defineEmailTemplateDefinition(config: z.input): EmailTemplateDefinition { + return EmailTemplateDefinitionSchema.parse(config); +} diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index 24a8d01853..a7d2ec11e8 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -295,6 +295,17 @@ export type TranslationData = z.infer; export const TranslationBundleSchema = lazySchema(() => z.record(LocaleSchema, TranslationDataSchema).describe('Map of locale codes to translation data')); export type TranslationBundle = z.infer; +/** Authoring input for {@link TranslationBundle} — defaulted fields are optional. */ +export type TranslationBundleInput = z.input; + +/** + * Type-safe factory for an i18n translation bundle (locale code → translations map). Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: TranslationBundle` literal. + */ +export function defineTranslationBundle(config: z.input): TranslationBundle { + return TranslationBundleSchema.parse(config); +} // ──────────────────────────────────────────────────────────────────────────── // File Organization Convention diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index d26cd40301..d1eb4a4c4e 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -502,3 +502,12 @@ export type ActionInput = z.input; export const Action = { create: (config: z.input): Action => ActionSchema.parse(config), } as const; + +/** + * Type-safe factory for a global or object action. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Action` literal. + */ +export function defineAction(config: z.input): Action { + return ActionSchema.parse(config); +} diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index 42c7ecbb3c..a3539897ba 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -399,6 +399,17 @@ export const PageSchema = lazySchema(() => z.object({ })); export type Page = z.infer; +/** Authoring input for {@link Page} — defaulted fields are optional. */ +export type PageInput = z.input; + +/** + * Type-safe factory for a custom page. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Page` literal. + */ +export function definePage(config: z.input): Page { + return PageSchema.parse(config); +} export type PageType = z.infer; export type PageComponent = z.infer; export type PageRegion = z.infer; diff --git a/packages/spec/src/ui/report.zod.ts b/packages/spec/src/ui/report.zod.ts index 38cc05afae..bda5d20fe0 100644 --- a/packages/spec/src/ui/report.zod.ts +++ b/packages/spec/src/ui/report.zod.ts @@ -220,3 +220,12 @@ export type ReportChartInput = z.input; export const Report = { create: (config: ReportInput): Report => ReportSchema.parse(config), } as const; + +/** + * Type-safe factory for an analytics report. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Report` literal. + */ +export function defineReport(config: z.input): Report { + return ReportSchema.parse(config); +} diff --git a/packages/spec/src/ui/theme.zod.ts b/packages/spec/src/ui/theme.zod.ts index 14628bf090..7d74b987c5 100644 --- a/packages/spec/src/ui/theme.zod.ts +++ b/packages/spec/src/ui/theme.zod.ts @@ -270,6 +270,17 @@ export const ThemeSchema = lazySchema(() => z.object({ })); export type Theme = z.infer; +/** Authoring input for {@link Theme} — defaulted fields are optional. */ +export type ThemeInput = z.input; + +/** + * Type-safe factory for a UI theme. Validates at authoring time via + * `.parse()` and accepts input-shape config (optional defaults, CEL + * shorthand) — preferred over a bare `: Theme` literal. + */ +export function defineTheme(config: z.input): Theme { + return ThemeSchema.parse(config); +} export type ColorPalette = z.infer; export type Typography = z.infer; export type Spacing = z.infer; From a0293ae89ee567f97243c30a0a6784ff4cb54aeb Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:38:15 +0800 Subject: [PATCH 2/3] refactor(examples): author metadata via defineX factories (#2035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates every example authoring file across crm/showcase/todo from bare output-type literals (`: Page`, `: UI.ActionInput`, …) to the corresponding defineX factory. The factory accepts input-shape config and validates each definition at module load, so a malformed metadata object now fails loudly at authoring time instead of silently passing as `any` (#2023). Covers 36 files / 16 domains — including files the *.{domain}.ts filename scan missed (reports/index.ts, actions/index.ts, security/sales-roles.ts, …), which the new lint guard surfaced. Co-Authored-By: Claude Opus 4.8 --- .../src/actions/convert-lead.action.ts | 6 ++-- .../app-crm/src/actions/park-lead.action.ts | 6 ++-- examples/app-crm/src/analytics/crm.cube.ts | 10 +++--- .../app-crm/src/connectors/crm-connectors.ts | 10 +++--- examples/app-crm/src/data/crm-mappings.ts | 10 +++--- .../app-crm/src/datasources/crm.datasource.ts | 10 +++--- examples/app-crm/src/emails/deal-won.email.ts | 6 ++-- .../src/emails/lead-follow-up.email.ts | 6 ++-- examples/app-crm/src/emails/welcome.email.ts | 6 ++-- .../src/extensions/contact.extension.ts | 6 ++-- examples/app-crm/src/pages/welcome.page.ts | 6 ++-- .../src/reports/sales-by-stage.report.ts | 6 ++-- examples/app-crm/src/security/crm-policy.ts | 10 +++--- examples/app-crm/src/security/sales-roles.ts | 24 ++++++------- .../app-crm/src/security/sharing-rules.ts | 14 ++++---- examples/app-crm/src/themes/crm.theme.ts | 10 +++--- .../src/translations/crm.translation.ts | 6 ++-- examples/app-crm/src/webhooks/crm-webhooks.ts | 10 +++--- examples/app-showcase/src/actions/index.ts | 30 ++++++++-------- .../src/pages/account-detail.page.ts | 6 ++-- .../src/pages/active-projects.page.ts | 6 ++-- examples/app-showcase/src/pages/index.ts | 6 ++-- .../app-showcase/src/pages/my-work.page.ts | 6 ++-- .../src/pages/new-project-wizard.page.ts | 6 ++-- .../src/pages/project-detail.page.ts | 6 ++-- .../src/pages/project-workspace.page.ts | 6 ++-- .../src/pages/review-queue.page.ts | 6 ++-- .../app-showcase/src/pages/settings.page.ts | 6 ++-- .../src/pages/task-detail.page.ts | 6 ++-- .../src/pages/task-triage.page.ts | 6 ++-- .../src/pages/task-visualizations.pages.ts | 30 ++++++++-------- .../src/pages/task-workbench.page.ts | 6 ++-- examples/app-showcase/src/reports/index.ts | 14 ++++---- examples/app-todo/src/actions/task.actions.ts | 34 +++++++++---------- examples/app-todo/src/reports/task.report.ts | 22 ++++++------ .../src/translations/todo.translation.ts | 6 ++-- 36 files changed, 185 insertions(+), 185 deletions(-) diff --git a/examples/app-crm/src/actions/convert-lead.action.ts b/examples/app-crm/src/actions/convert-lead.action.ts index 970cecf78b..4671bb4c28 100644 --- a/examples/app-crm/src/actions/convert-lead.action.ts +++ b/examples/app-crm/src/actions/convert-lead.action.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type * as UI from '@objectstack/spec/ui'; +import { defineAction } from '@objectstack/spec/ui'; /** * Row-level action on crm_lead — launches the Convert Lead screen flow wizard. * Shown as a button in the lead list row menu and in the lead record header. */ -export const ConvertLeadAction: UI.ActionInput = { +export const ConvertLeadAction = defineAction({ name: 'crm_convert_lead', label: 'Convert Lead', icon: 'ArrowRightCircle', @@ -20,4 +20,4 @@ export const ConvertLeadAction: UI.ActionInput = { // hitting the flow's "already converted" guard screen. The flow keeps that // guard as a server-side backstop. visible: 'record.status != "converted"', -}; +}); diff --git a/examples/app-crm/src/actions/park-lead.action.ts b/examples/app-crm/src/actions/park-lead.action.ts index 62d35ab7b1..0f4a30ca04 100644 --- a/examples/app-crm/src/actions/park-lead.action.ts +++ b/examples/app-crm/src/actions/park-lead.action.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type * as UI from '@objectstack/spec/ui'; +import { defineAction } from '@objectstack/spec/ui'; /** * Row-level action on crm_lead — reassign the lead's owner. @@ -11,7 +11,7 @@ import type * as UI from '@objectstack/spec/ui'; * UndoManager — Ctrl+Z works too). Prompts for the new owner via one param * (pre-filled with "Triage Queue"). */ -export const ParkLeadAction: UI.ActionInput = { +export const ParkLeadAction = defineAction({ name: 'crm_park_lead', label: 'Reassign Lead', icon: 'UserPlus', @@ -32,4 +32,4 @@ export const ParkLeadAction: UI.ActionInput = { ], undoable: true, successMessage: 'Lead reassigned.', -}; +}); diff --git a/examples/app-crm/src/analytics/crm.cube.ts b/examples/app-crm/src/analytics/crm.cube.ts index cda2a47672..766f9cc5ab 100644 --- a/examples/app-crm/src/analytics/crm.cube.ts +++ b/examples/app-crm/src/analytics/crm.cube.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Cube } from '@objectstack/spec/data'; +import { defineCube } from '@objectstack/spec/data'; /** * Opportunity Pipeline Cube — revenue metrics broken down by stage, * owner, and account for the CRM sales dashboard. */ -export const PipelineCube: Cube = { +export const PipelineCube = defineCube({ name: 'crm_pipeline', title: 'CRM Pipeline', description: 'Revenue and deal-count analytics across the sales pipeline.', @@ -71,12 +71,12 @@ export const PipelineCube: Cube = { every: '1 hour', }, public: false, -}; +}); /** * Lead funnel cube — conversion metrics from lead to opportunity. */ -export const LeadFunnelCube: Cube = { +export const LeadFunnelCube = defineCube({ name: 'crm_lead_funnel', title: 'CRM Lead Funnel', description: 'Lead volume and conversion rate analytics.', @@ -119,4 +119,4 @@ export const LeadFunnelCube: Cube = { every: '30 minutes', }, public: false, -}; +}); diff --git a/examples/app-crm/src/connectors/crm-connectors.ts b/examples/app-crm/src/connectors/crm-connectors.ts index a732f63c26..3cf9b6c42f 100644 --- a/examples/app-crm/src/connectors/crm-connectors.ts +++ b/examples/app-crm/src/connectors/crm-connectors.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { ConnectorInput } from '@objectstack/spec/integration'; +import { defineConnector } from '@objectstack/spec/integration'; /** * HubSpot connector — sync contacts and deals bi-directionally. * Uses OAuth2 for authentication; actual credentials come from environment. */ -export const HubSpotConnector: ConnectorInput = { +export const HubSpotConnector = defineConnector({ name: 'hubspot_crm', label: 'HubSpot CRM', type: 'saas', @@ -86,12 +86,12 @@ export const HubSpotConnector: ConnectorInput = { requestTimeoutMs: 30000, status: 'inactive', enabled: true, -}; +}); /** * Slack connector — post notifications to channels. */ -export const SlackConnector: ConnectorInput = { +export const SlackConnector = defineConnector({ name: 'slack_notifications', label: 'Slack', type: 'api', @@ -123,4 +123,4 @@ export const SlackConnector: ConnectorInput = { }, status: 'inactive', enabled: true, -}; +}); diff --git a/examples/app-crm/src/data/crm-mappings.ts b/examples/app-crm/src/data/crm-mappings.ts index d92a182c3c..66a17731fd 100644 --- a/examples/app-crm/src/data/crm-mappings.ts +++ b/examples/app-crm/src/data/crm-mappings.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Mapping } from '@objectstack/spec/data'; +import { defineMapping } from '@objectstack/spec/data'; /** * CSV import mapping for bulk lead upload. * Maps common CRM export column names to crm_lead fields. */ -export const LeadCsvImportMapping: Mapping = { +export const LeadCsvImportMapping = defineMapping({ name: 'csv_import_leads', label: 'CSV Import: Leads', sourceFormat: 'csv', @@ -51,12 +51,12 @@ export const LeadCsvImportMapping: Mapping = { ], errorPolicy: 'skip', batchSize: 500, -}; +}); /** * JSON import mapping for contact sync from external systems (HubSpot, etc.). */ -export const ContactJsonSyncMapping: Mapping = { +export const ContactJsonSyncMapping = defineMapping({ name: 'json_sync_contacts', label: 'JSON Sync: Contacts from HubSpot', sourceFormat: 'json', @@ -72,4 +72,4 @@ export const ContactJsonSyncMapping: Mapping = { ], errorPolicy: 'skip', batchSize: 250, -}; +}); diff --git a/examples/app-crm/src/datasources/crm.datasource.ts b/examples/app-crm/src/datasources/crm.datasource.ts index 7c41686bf9..09c210a7f0 100644 --- a/examples/app-crm/src/datasources/crm.datasource.ts +++ b/examples/app-crm/src/datasources/crm.datasource.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { DatasourceInput } from '@objectstack/spec/data'; +import { defineDatasource } from '@objectstack/spec/data'; /** * Primary CRM datasource — in-memory SQLite for the example. * In production, swap `driver` to 'postgres' and supply real `config`. */ -export const CrmDatasource: DatasourceInput = { +export const CrmDatasource = defineDatasource({ name: 'crm_primary', label: 'CRM Primary Database', driver: 'sqlite', @@ -18,12 +18,12 @@ export const CrmDatasource: DatasourceInput = { max: 5, }, active: true, -}; +}); /** * Read-replica for analytics queries — demonstrates datasource routing. */ -export const CrmAnalyticsDatasource: DatasourceInput = { +export const CrmAnalyticsDatasource = defineDatasource({ name: 'crm_analytics', label: 'CRM Analytics Read Replica', driver: 'sqlite', @@ -32,4 +32,4 @@ export const CrmAnalyticsDatasource: DatasourceInput = { readOnly: true, }, active: true, -}; +}); diff --git a/examples/app-crm/src/emails/deal-won.email.ts b/examples/app-crm/src/emails/deal-won.email.ts index d24fbce3c3..39bac0b18a 100644 --- a/examples/app-crm/src/emails/deal-won.email.ts +++ b/examples/app-crm/src/emails/deal-won.email.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { EmailTemplateDefinitionInput } from '@objectstack/spec/system'; +import { defineEmailTemplateDefinition } from '@objectstack/spec/system'; /** * Sent when an opportunity moves to Closed Won. * Referenced by the `notify_owner_deal_won` workflow action. */ -export const DealWonEmail: EmailTemplateDefinitionInput = { +export const DealWonEmail = defineEmailTemplateDefinition({ name: 'crm.deal_won', label: 'Deal Won — Owner Congrats', category: 'workflow', @@ -33,4 +33,4 @@ Nice work!`, ], active: true, description: 'Internal congrats email fired by the High-Value Deal workflow when stage = Closed Won.', -}; +}); diff --git a/examples/app-crm/src/emails/lead-follow-up.email.ts b/examples/app-crm/src/emails/lead-follow-up.email.ts index c6e2fb2066..43d9fd94d2 100644 --- a/examples/app-crm/src/emails/lead-follow-up.email.ts +++ b/examples/app-crm/src/emails/lead-follow-up.email.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { EmailTemplateDefinitionInput } from '@objectstack/spec/system'; +import { defineEmailTemplateDefinition } from '@objectstack/spec/system'; /** * Follow-up nudge sent by the Stale Opportunity workflow when no * activity has been logged on a Lead for the configured threshold. */ -export const LeadFollowUpEmail: EmailTemplateDefinitionInput = { +export const LeadFollowUpEmail = defineEmailTemplateDefinition({ name: 'crm.lead_followup', label: 'Lead — Follow-Up Reminder', category: 'notification', @@ -35,4 +35,4 @@ Follow up: {{lead_url}}`, ], active: true, description: 'Internal reminder fired by the Stale Opportunity workflow.', -}; +}); diff --git a/examples/app-crm/src/emails/welcome.email.ts b/examples/app-crm/src/emails/welcome.email.ts index 923049fd94..77282fce5a 100644 --- a/examples/app-crm/src/emails/welcome.email.ts +++ b/examples/app-crm/src/emails/welcome.email.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { EmailTemplateDefinitionInput } from '@objectstack/spec/system'; +import { defineEmailTemplateDefinition } from '@objectstack/spec/system'; /** * Welcome email sent to a Contact after it's added to the CRM. * Demonstrates marketing-category templates with a clear CTA. */ -export const WelcomeEmail: EmailTemplateDefinitionInput = { +export const WelcomeEmail = defineEmailTemplateDefinition({ name: 'crm.welcome', label: 'Welcome — New Contact', category: 'marketing', @@ -36,4 +36,4 @@ Open your portal: {{portal_url}}`, replyTo: 'support@acme.example', active: true, description: 'Marketing welcome email sent on contact creation.', -}; +}); diff --git a/examples/app-crm/src/extensions/contact.extension.ts b/examples/app-crm/src/extensions/contact.extension.ts index 67e84e3d3e..1aa55ce5ed 100644 --- a/examples/app-crm/src/extensions/contact.extension.ts +++ b/examples/app-crm/src/extensions/contact.extension.ts @@ -1,13 +1,13 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { ObjectExtensionInput } from '@objectstack/spec/data'; +import { defineObjectExtension } from '@objectstack/spec/data'; /** * Extends the built-in crm_contact object with social-media fields. * Demonstrates ObjectExtension — additive fields without re-declaring the * whole object schema. */ -export const ContactExtension: ObjectExtensionInput = { +export const ContactExtension = defineObjectExtension({ extend: 'crm_contact', label: 'Contact (CRM Extended)', fields: { @@ -35,4 +35,4 @@ export const ContactExtension: ObjectExtensionInput = { }, }, priority: 210, -}; +}); diff --git a/examples/app-crm/src/pages/welcome.page.ts b/examples/app-crm/src/pages/welcome.page.ts index a1f9bd1e96..199c8fe94c 100644 --- a/examples/app-crm/src/pages/welcome.page.ts +++ b/examples/app-crm/src/pages/welcome.page.ts @@ -1,11 +1,11 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type * as UI from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Example custom page — a CRM landing page. */ -export const CrmWelcomePage: UI.Page = { +export const CrmWelcomePage = definePage({ name: 'crm_welcome', label: 'CRM Welcome', type: 'home', @@ -40,4 +40,4 @@ export const CrmWelcomePage: UI.Page = { ], }, ], -}; +}); diff --git a/examples/app-crm/src/reports/sales-by-stage.report.ts b/examples/app-crm/src/reports/sales-by-stage.report.ts index e4f2a45ccb..14e75c7a73 100644 --- a/examples/app-crm/src/reports/sales-by-stage.report.ts +++ b/examples/app-crm/src/reports/sales-by-stage.report.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type * as UI from '@objectstack/spec/ui'; +import { defineReport } from '@objectstack/spec/ui'; /** * Example report — total opportunity amount grouped by stage. @@ -11,7 +11,7 @@ import type * as UI from '@objectstack/spec/ui'; * its "grouped by stage" label), so both forms compute the same number and the * reconciliation harness can verify them. */ -export const SalesByStageReport: UI.ReportInput = { +export const SalesByStageReport = defineReport({ name: 'crm_sales_by_stage', label: 'Sales by Stage', description: 'Total opportunity amount grouped by sales stage.', @@ -19,4 +19,4 @@ export const SalesByStageReport: UI.ReportInput = { dataset: 'opportunity_metrics', rows: ['stage'], values: ['total_amount'], -}; +}); diff --git a/examples/app-crm/src/security/crm-policy.ts b/examples/app-crm/src/security/crm-policy.ts index f5c1102e14..577c7d45ff 100644 --- a/examples/app-crm/src/security/crm-policy.ts +++ b/examples/app-crm/src/security/crm-policy.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Policy } from '@objectstack/spec/security'; +import { definePolicy } from '@objectstack/spec/security'; /** * Default CRM security policy applied to all users. * Enforces password complexity, session limits, and audit logging. */ -export const CrmDefaultPolicy: Policy = { +export const CrmDefaultPolicy = definePolicy({ name: 'crm_default_policy', password: { minLength: 12, @@ -28,12 +28,12 @@ export const CrmDefaultPolicy: Policy = { captureRead: false, }, isDefault: true, -}; +}); /** * Strict policy for Finance Approvers — requires MFA and shorter sessions. */ -export const CrmFinancePolicy: Policy = { +export const CrmFinancePolicy = definePolicy({ name: 'crm_finance_policy', password: { minLength: 16, @@ -56,4 +56,4 @@ export const CrmFinancePolicy: Policy = { }, isDefault: false, assignedProfiles: ['finance_approver'], -}; +}); diff --git a/examples/app-crm/src/security/sales-roles.ts b/examples/app-crm/src/security/sales-roles.ts index 4984005a94..de8c7d4ebe 100644 --- a/examples/app-crm/src/security/sales-roles.ts +++ b/examples/app-crm/src/security/sales-roles.ts @@ -1,37 +1,37 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type * as Identity from '@objectstack/spec/identity'; -import type * as Security from '@objectstack/spec/security'; +import { defineRole } from '@objectstack/spec/identity'; +import { definePermissionSet } from '@objectstack/spec/security'; /** * Example roles — a small sales hierarchy. */ -export const SalesRepRole: Identity.RoleInput = { +export const SalesRepRole = defineRole({ name: 'sales_rep', label: 'Sales Representative', description: 'Front-line sales representative.', -}; +}); -export const SalesManagerRole: Identity.RoleInput = { +export const SalesManagerRole = defineRole({ name: 'sales_manager', label: 'Sales Manager', description: 'Manages a team of sales reps.', parent: 'sales_rep', -}; +}); /** Referenced by the Discount Approval second step. */ -export const FinanceApproverRole: Identity.RoleInput = { +export const FinanceApproverRole = defineRole({ name: 'finance_approver', label: 'Finance Approver', description: 'Finance team member authorised to approve discounts above 30%.', -}; +}); /** * Example permission set — base permissions on CRM objects for sales users. * * Note: `objects` is a Record keyed by object name, not an array. */ -export const SalesUserPermissionSet: Security.PermissionSetInput = { +export const SalesUserPermissionSet = definePermissionSet({ name: 'crm_sales_user', label: 'CRM Sales User', isProfile: false, @@ -42,7 +42,7 @@ export const SalesUserPermissionSet: Security.PermissionSetInput = { crm_lead: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false }, crm_activity: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false }, }, -}; +}); /** * Guest profile for the public Web-to-Lead form (lead.view.ts `web_to_lead`). @@ -52,11 +52,11 @@ export const SalesUserPermissionSet: Security.PermissionSetInput = { * `crm_lead` (not a short `lead`). INSERT-only — guests can never read, edit, or * delete any record. */ -export const GuestPortalProfile: Security.PermissionSetInput = { +export const GuestPortalProfile = definePermissionSet({ name: 'guest_portal', label: 'Guest (Public Forms)', isProfile: true, objects: { crm_lead: { allowRead: false, allowCreate: true, allowEdit: false, allowDelete: false }, }, -}; +}); diff --git a/examples/app-crm/src/security/sharing-rules.ts b/examples/app-crm/src/security/sharing-rules.ts index 2e49b44a2f..4d4407a9e3 100644 --- a/examples/app-crm/src/security/sharing-rules.ts +++ b/examples/app-crm/src/security/sharing-rules.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { SharingRuleInput } from '@objectstack/spec/security'; +import { defineSharingRule } from '@objectstack/spec/security'; /** * Criteria-based sharing: share high-value opportunities (amount > 100000) * with the Sales Manager role so managers always have read access to big deals. */ -export const HighValueOpportunitySharingRule: SharingRuleInput = { +export const HighValueOpportunitySharingRule = defineSharingRule({ type: 'criteria', name: 'share_high_value_opps_with_managers', label: 'High-Value Deals → Sales Managers', @@ -19,13 +19,13 @@ export const HighValueOpportunitySharingRule: SharingRuleInput = { value: 'sales_manager', }, active: true, -}; +}); /** * Owner-based sharing: leads owned by a Sales Rep are shared read-only * with their manager so managers can coach on individual pipelines. */ -export const RepLeadSharingRule: SharingRuleInput = { +export const RepLeadSharingRule = defineSharingRule({ type: 'owner', name: 'share_rep_leads_with_manager', label: "Rep's Leads → Manager (read-only)", @@ -41,13 +41,13 @@ export const RepLeadSharingRule: SharingRuleInput = { value: 'sales_manager', }, active: true, -}; +}); /** * Criteria-based: share activities linked to won deals with the whole * Sales team so everyone can learn from successful engagement patterns. */ -export const WonDealActivitySharingRule: SharingRuleInput = { +export const WonDealActivitySharingRule = defineSharingRule({ type: 'criteria', name: 'share_won_deal_activities', label: 'Won-Deal Activities → All Sales', @@ -60,4 +60,4 @@ export const WonDealActivitySharingRule: SharingRuleInput = { value: 'sales_rep', }, active: true, -}; +}); diff --git a/examples/app-crm/src/themes/crm.theme.ts b/examples/app-crm/src/themes/crm.theme.ts index 925408fbba..c1ecfa8f0f 100644 --- a/examples/app-crm/src/themes/crm.theme.ts +++ b/examples/app-crm/src/themes/crm.theme.ts @@ -1,11 +1,11 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Theme } from '@objectstack/spec/ui'; +import { defineTheme } from '@objectstack/spec/ui'; /** * Default CRM brand theme — light mode with professional blue palette. */ -export const CrmLightTheme: Theme = { +export const CrmLightTheme = defineTheme({ name: 'crm_light', label: 'CRM Light', description: 'Default CRM theme — professional blue, light mode.', @@ -54,12 +54,12 @@ export const CrmLightTheme: Theme = { }, density: 'regular', wcagContrast: 'AA', -}; +}); /** * Dark variant — same palette, dark surfaces. */ -export const CrmDarkTheme: Theme = { +export const CrmDarkTheme = defineTheme({ name: 'crm_dark', label: 'CRM Dark', description: 'CRM dark mode theme.', @@ -80,4 +80,4 @@ export const CrmDarkTheme: Theme = { info: '#3DD5F3', }, density: 'regular', -}; +}); diff --git a/examples/app-crm/src/translations/crm.translation.ts b/examples/app-crm/src/translations/crm.translation.ts index 6326f6447e..edacfce59c 100644 --- a/examples/app-crm/src/translations/crm.translation.ts +++ b/examples/app-crm/src/translations/crm.translation.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { TranslationBundle } from '@objectstack/spec/system'; +import { defineTranslationBundle } from '@objectstack/spec/system'; /** * CRM translation bundle — English + Simplified Chinese. @@ -8,7 +8,7 @@ import type { TranslationBundle } from '@objectstack/spec/system'; * Provides display labels for all CRM objects, apps, and common UI messages * so the Studio i18n pipeline has real data to render. */ -export const CrmTranslationBundle: TranslationBundle = { +export const CrmTranslationBundle = defineTranslationBundle({ en: { objects: { crm_account: { @@ -186,4 +186,4 @@ export const CrmTranslationBundle: TranslationBundle = { 'crm.activity.due_today': '您今天有 {count} 个活动待处理。', }, }, -}; +}); diff --git a/examples/app-crm/src/webhooks/crm-webhooks.ts b/examples/app-crm/src/webhooks/crm-webhooks.ts index 0499a8ef6b..33715d87de 100644 --- a/examples/app-crm/src/webhooks/crm-webhooks.ts +++ b/examples/app-crm/src/webhooks/crm-webhooks.ts @@ -1,11 +1,11 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { WebhookInput } from '@objectstack/spec/automation'; +import { defineWebhook } from '@objectstack/spec/automation'; /** * Notify external CRM bus whenever an opportunity is created or updated. */ -export const OpportunityChangedWebhook: WebhookInput = { +export const OpportunityChangedWebhook = defineWebhook({ name: 'crm_opportunity_changed', label: 'Opportunity Created / Updated', object: 'crm_opportunity', @@ -32,12 +32,12 @@ export const OpportunityChangedWebhook: WebhookInput = { isActive: true, description: 'Fires on every opportunity create/update for downstream sync.', tags: ['crm', 'sync'], -}; +}); /** * Notify Slack channel when a deal is won. */ -export const DealWonSlackWebhook: WebhookInput = { +export const DealWonSlackWebhook = defineWebhook({ name: 'crm_deal_won_slack', label: 'Deal Won → Slack', object: 'crm_opportunity', @@ -49,4 +49,4 @@ export const DealWonSlackWebhook: WebhookInput = { isActive: true, description: 'Posts to #wins Slack channel when stage=closed_won.', tags: ['slack', 'notifications'], -}; +}); diff --git a/examples/app-showcase/src/actions/index.ts b/examples/app-showcase/src/actions/index.ts index 05bcdeefee..efc2cd4e3e 100644 --- a/examples/app-showcase/src/actions/index.ts +++ b/examples/app-showcase/src/actions/index.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Action } from '@objectstack/spec/ui'; +import { defineAction } from '@objectstack/spec/ui'; const task = 'showcase_task'; @@ -11,7 +11,7 @@ const task = 'showcase_task'; */ /** script — inline handler, shown on each row and the record header. */ -export const MarkDoneAction: Action = { +export const MarkDoneAction = defineAction({ name: 'showcase_mark_done', label: 'Mark Done', icon: 'check', @@ -22,10 +22,10 @@ export const MarkDoneAction: Action = { // explicitly-named actions, mirroring the platform's own sys-user pages. locations: ['list_item', 'record_header', 'record_section'], refreshAfter: true, -}; +}); /** url — navigate out, from the row overflow menu. */ -export const OpenDocsAction: Action = { +export const OpenDocsAction = defineAction({ name: 'showcase_open_docs', label: 'Open Docs', icon: 'book-open', @@ -34,7 +34,7 @@ export const OpenDocsAction: Action = { target: 'https://docs.objectstack.ai', locations: ['record_more'], refreshAfter: false, -}; +}); /** * flow — launch the Reassign screen-flow wizard. Row-level (`list_item`) so the @@ -42,7 +42,7 @@ export const OpenDocsAction: Action = { * `screen` node and writes it back with `update_record`. The objectui * FlowRunner renders the screen and resumes the run. */ -export const BulkReassignAction: Action = { +export const BulkReassignAction = defineAction({ name: 'showcase_bulk_reassign', label: 'Reassign…', icon: 'users', @@ -51,10 +51,10 @@ export const BulkReassignAction: Action = { target: 'showcase_reassign_wizard', locations: ['list_item', 'list_toolbar'], refreshAfter: true, -}; +}); /** modal — open a dialog/page. */ -export const QuickViewAction: Action = { +export const QuickViewAction = defineAction({ name: 'showcase_quick_view', label: 'Quick View', icon: 'eye', @@ -63,10 +63,10 @@ export const QuickViewAction: Action = { target: 'showcase_component_gallery', locations: ['list_item'], refreshAfter: false, -}; +}); /** api — call a custom endpoint. */ -export const RecalcEstimateAction: Action = { +export const RecalcEstimateAction = defineAction({ name: 'showcase_recalc_estimate', label: 'Recalculate Estimate', icon: 'calculator', @@ -75,10 +75,10 @@ export const RecalcEstimateAction: Action = { target: '/api/v1/showcase/recalc', locations: ['record_more', 'record_section'], refreshAfter: true, -}; +}); /** form — open a parameter form dialog. */ -export const LogTimeAction: Action = { +export const LogTimeAction = defineAction({ name: 'showcase_log_time', label: 'Log Time', icon: 'clock', @@ -88,10 +88,10 @@ export const LogTimeAction: Action = { // `record_section` so it surfaces in the Task Detail quick-actions bar too. locations: ['record_header', 'record_related', 'record_section'], refreshAfter: true, -}; +}); /** global nav command-palette action. */ -export const NewTaskAction: Action = { +export const NewTaskAction = defineAction({ name: 'showcase_new_task', label: 'New Task', icon: 'plus', @@ -100,7 +100,7 @@ export const NewTaskAction: Action = { target: 'showcase_component_gallery', locations: ['global_nav'], refreshAfter: true, -}; +}); export const allActions = [ MarkDoneAction, diff --git a/examples/app-showcase/src/pages/account-detail.page.ts b/examples/app-showcase/src/pages/account-detail.page.ts index 01b6943fc6..e8bc6bf0a9 100644 --- a/examples/app-showcase/src/pages/account-detail.page.ts +++ b/examples/app-showcase/src/pages/account-detail.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Account 360 — the flagship "object 360" record page every enterprise app has, @@ -20,7 +20,7 @@ import type { Page } from '@objectstack/spec/ui'; * slotted path; the same shape inside a full-page region does not — mirror the * working Project Detail page.) */ -export const AccountDetailPage: Page = { +export const AccountDetailPage = definePage({ name: 'showcase_account_detail', label: 'Account', type: 'record', @@ -103,4 +103,4 @@ export const AccountDetailPage: Page = { }, }, }, -}; +}); diff --git a/examples/app-showcase/src/pages/active-projects.page.ts b/examples/app-showcase/src/pages/active-projects.page.ts index a32e9223da..dea7a10f63 100644 --- a/examples/app-showcase/src/pages/active-projects.page.ts +++ b/examples/app-showcase/src/pages/active-projects.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Active Projects — interface page demonstrating the deeper list config: @@ -10,7 +10,7 @@ import type { Page } from '@objectstack/spec/ui'; * • `addRecord` — a toolbar "add" entry point that opens a FORM. * • dropdown user-filters layered on top of the base filter. */ -export const ActiveProjectsPage: Page = { +export const ActiveProjectsPage = definePage({ name: 'showcase_active_projects', label: 'Active Projects', type: 'list', @@ -36,4 +36,4 @@ export const ActiveProjectsPage: Page = { addRecord: { enabled: true, position: 'top', mode: 'form', formView: 'default' }, showRecordCount: true, }, -}; +}); diff --git a/examples/app-showcase/src/pages/index.ts b/examples/app-showcase/src/pages/index.ts index 763270f5da..379a4530b3 100644 --- a/examples/app-showcase/src/pages/index.ts +++ b/examples/app-showcase/src/pages/index.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; export { ProjectWorkspacePage } from './project-workspace.page.js'; export { ProjectDetailPage } from './project-detail.page.js'; @@ -30,7 +30,7 @@ export { * region (ai:input, oversized element:image, page:card body) so the first * impression is polished, not a debug canvas. */ -export const ComponentGalleryPage: Page = { +export const ComponentGalleryPage = definePage({ name: 'showcase_component_gallery', label: 'Component Gallery', type: 'home', @@ -85,4 +85,4 @@ export const ComponentGalleryPage: Page = { ], }, ], -}; +}); diff --git a/examples/app-showcase/src/pages/my-work.page.ts b/examples/app-showcase/src/pages/my-work.page.ts index 3cbf8afea7..df317aa692 100644 --- a/examples/app-showcase/src/pages/my-work.page.ts +++ b/examples/app-showcase/src/pages/my-work.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * My Work — a role-aware workspace home that *composes* live data the way a @@ -10,7 +10,7 @@ import type { Page } from '@objectstack/spec/ui'; * via the `{current_user_id}` token (records I own); * • sidebar shortcuts + a per-user `visible`-gated note on `user.email`. */ -export const MyWorkPage: Page = { +export const MyWorkPage = definePage({ name: 'showcase_my_work', label: 'My Work', type: 'home', @@ -86,4 +86,4 @@ export const MyWorkPage: Page = { ], }, ], -}; +}); diff --git a/examples/app-showcase/src/pages/new-project-wizard.page.ts b/examples/app-showcase/src/pages/new-project-wizard.page.ts index 548254bc0b..a158dbcd35 100644 --- a/examples/app-showcase/src/pages/new-project-wizard.page.ts +++ b/examples/app-showcase/src/pages/new-project-wizard.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * New Project Wizard — a multi-step (wizard) form surface. The showcase @@ -9,7 +9,7 @@ import type { Page } from '@objectstack/spec/ui'; * `formType: 'wizard'` directly: Basics → Status → Budget, with a step * indicator, over showcase_project. */ -export const NewProjectWizardPage: Page = { +export const NewProjectWizardPage = definePage({ name: 'showcase_new_project_wizard', label: 'New Project (Wizard)', type: 'app', @@ -40,4 +40,4 @@ export const NewProjectWizardPage: Page = { ], }, ], -}; +}); diff --git a/examples/app-showcase/src/pages/project-detail.page.ts b/examples/app-showcase/src/pages/project-detail.page.ts index 5cb1c0aa31..94837b93c3 100644 --- a/examples/app-showcase/src/pages/project-detail.page.ts +++ b/examples/app-showcase/src/pages/project-detail.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Project detail — a slotted record page that surfaces the project's Tasks as @@ -14,7 +14,7 @@ import type { Page } from '@objectstack/spec/ui'; * fills in the header / highlights / details / discussion; the Tasks tab below * replaces the synthesized related-list strip. */ -export const ProjectDetailPage: Page = { +export const ProjectDetailPage = definePage({ name: 'showcase_project_detail', label: 'Project', type: 'record', @@ -102,4 +102,4 @@ export const ProjectDetailPage: Page = { }, }, }, -}; +}); diff --git a/examples/app-showcase/src/pages/project-workspace.page.ts b/examples/app-showcase/src/pages/project-workspace.page.ts index e25a5136b7..b6ac8698c1 100644 --- a/examples/app-showcase/src/pages/project-workspace.page.ts +++ b/examples/app-showcase/src/pages/project-workspace.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Project Workspace — a master-detail (header + line items) entry scenario. @@ -10,7 +10,7 @@ import type { Page } from '@objectstack/spec/ui'; * `showcase_task.project` is a `master_detail` field, so the children are * created with the parent FK set in a single client-orchestrated transaction. */ -export const ProjectWorkspacePage: Page = { +export const ProjectWorkspacePage = definePage({ name: 'showcase_project_workspace', label: 'New Project + Tasks', type: 'app', @@ -63,4 +63,4 @@ export const ProjectWorkspacePage: Page = { ], }, ], -}; +}); diff --git a/examples/app-showcase/src/pages/review-queue.page.ts b/examples/app-showcase/src/pages/review-queue.page.ts index 6d2f2cf03f..756abc14b6 100644 --- a/examples/app-showcase/src/pages/review-queue.page.ts +++ b/examples/app-showcase/src/pages/review-queue.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Approvals · Review Queue — the human side of the approval / review flows. @@ -12,7 +12,7 @@ import type { Page } from '@objectstack/spec/ui'; * buttons (Mark done = approve & complete) and a drawer to inspect each item. * Tabs let the reviewer pivot to urgent or blocked work. */ -export const ReviewQueuePage: Page = { +export const ReviewQueuePage = definePage({ name: 'showcase_review_queue', label: 'Approvals', type: 'list', @@ -35,4 +35,4 @@ export const ReviewQueuePage: Page = { recordAction: 'drawer', showRecordCount: true, }, -}; +}); diff --git a/examples/app-showcase/src/pages/settings.page.ts b/examples/app-showcase/src/pages/settings.page.ts index 111eb95fe1..2353bd5c8a 100644 --- a/examples/app-showcase/src/pages/settings.page.ts +++ b/examples/app-showcase/src/pages/settings.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Settings — every enterprise app needs one. A record page over the singleton @@ -8,7 +8,7 @@ import type { Page } from '@objectstack/spec/ui'; * to change it), so this is a working preferences surface grouped into * Appearance / Notifications sections. */ -export const SettingsPage: Page = { +export const SettingsPage = definePage({ name: 'showcase_settings', label: 'Setting', type: 'record', @@ -33,4 +33,4 @@ export const SettingsPage: Page = { ], }, ], -}; +}); diff --git a/examples/app-showcase/src/pages/task-detail.page.ts b/examples/app-showcase/src/pages/task-detail.page.ts index 6af7976314..4022eae090 100644 --- a/examples/app-showcase/src/pages/task-detail.page.ts +++ b/examples/app-showcase/src/pages/task-detail.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Task Detail — a record page that exercises the record-layout component set @@ -15,7 +15,7 @@ import type { Page } from '@objectstack/spec/ui'; * `kind: 'full'` — this page fully owns the record layout (vs the slotted * Project page which only overrides the tabs slot). */ -export const TaskDetailPage: Page = { +export const TaskDetailPage = definePage({ name: 'showcase_task_detail', label: 'Task', type: 'record', @@ -77,4 +77,4 @@ export const TaskDetailPage: Page = { ], }, ], -}; +}); diff --git a/examples/app-showcase/src/pages/task-triage.page.ts b/examples/app-showcase/src/pages/task-triage.page.ts index f522b6034e..8e4b2583ef 100644 --- a/examples/app-showcase/src/pages/task-triage.page.ts +++ b/examples/app-showcase/src/pages/task-triage.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Task Triage — interface page demonstrating the **tabs** user-filter element @@ -13,7 +13,7 @@ import type { Page } from '@objectstack/spec/ui'; * (`{ name, label, filter }`) — it never switches the view form; that is the * separate "Visualizations" axis (locked to grid here). */ -export const TaskTriagePage: Page = { +export const TaskTriagePage = definePage({ name: 'showcase_task_triage', label: 'Task Triage', type: 'list', @@ -56,4 +56,4 @@ export const TaskTriagePage: Page = { showRecordCount: true, }, -}; +}); diff --git a/examples/app-showcase/src/pages/task-visualizations.pages.ts b/examples/app-showcase/src/pages/task-visualizations.pages.ts index fcce5d2cf1..20cccdeeef 100644 --- a/examples/app-showcase/src/pages/task-visualizations.pages.ts +++ b/examples/app-showcase/src/pages/task-visualizations.pages.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Visualization gallery — one interface page per record visualization, each @@ -25,7 +25,7 @@ const base = { const cols = ['title', 'assignee', 'status', 'priority', 'due_date']; -export const TaskBoardPage: Page = { +export const TaskBoardPage = definePage({ ...base, name: 'showcase_task_board', label: 'Task Board', @@ -36,9 +36,9 @@ export const TaskBoardPage: Page = { userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); -export const TaskCalendarPage: Page = { +export const TaskCalendarPage = definePage({ ...base, name: 'showcase_task_calendar', label: 'Task Calendar', @@ -49,9 +49,9 @@ export const TaskCalendarPage: Page = { userActions: { sort: false, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); -export const TaskGalleryPage: Page = { +export const TaskGalleryPage = definePage({ ...base, name: 'showcase_task_gallery', label: 'Task Gallery', @@ -62,9 +62,9 @@ export const TaskGalleryPage: Page = { userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); -export const TaskSchedulePage: Page = { +export const TaskSchedulePage = definePage({ ...base, name: 'showcase_task_schedule', label: 'Team Schedule (Gantt)', @@ -75,9 +75,9 @@ export const TaskSchedulePage: Page = { userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); -export const TaskTimelinePage: Page = { +export const TaskTimelinePage = definePage({ ...base, name: 'showcase_task_timeline', label: 'Activity Timeline', @@ -88,9 +88,9 @@ export const TaskTimelinePage: Page = { userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); -export const TaskMapPage: Page = { +export const TaskMapPage = definePage({ ...base, name: 'showcase_task_map', label: 'Work Map', @@ -101,9 +101,9 @@ export const TaskMapPage: Page = { userActions: { sort: false, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); -export const TaskAllViewsPage: Page = { +export const TaskAllViewsPage = definePage({ ...base, name: 'showcase_task_all_views', label: 'All Views', @@ -119,4 +119,4 @@ export const TaskAllViewsPage: Page = { userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false }, showRecordCount: true, }, -}; +}); diff --git a/examples/app-showcase/src/pages/task-workbench.page.ts b/examples/app-showcase/src/pages/task-workbench.page.ts index 177aade487..cb4160559a 100644 --- a/examples/app-showcase/src/pages/task-workbench.page.ts +++ b/examples/app-showcase/src/pages/task-workbench.page.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; /** * Task Workbench — the canonical **interface page** example (ADR-0047). @@ -20,7 +20,7 @@ import type { Page } from '@objectstack/spec/ui'; * Mirrors Airtable's Interfaces right panel: Data (source), User filters * (Elements: dropdowns), Appearance (Visualizations), User actions. */ -export const TaskWorkbenchPage: Page = { +export const TaskWorkbenchPage = definePage({ name: 'showcase_task_workbench', label: 'Task Workbench', type: 'list', @@ -67,4 +67,4 @@ export const TaskWorkbenchPage: Page = { showRecordCount: true, }, -}; +}); diff --git a/examples/app-showcase/src/reports/index.ts b/examples/app-showcase/src/reports/index.ts index c15e5b6a04..ab5df66044 100644 --- a/examples/app-showcase/src/reports/index.ts +++ b/examples/app-showcase/src/reports/index.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Report } from '@objectstack/spec/ui'; +import { defineReport } from '@objectstack/spec/ui'; const task = 'showcase_task'; @@ -10,7 +10,7 @@ const task = 'showcase_task'; // not analytics, so it is no longer a report. /** 2 ── Summary: grouped down by status with a sum. */ -export const HoursByStatusReport: Report = { +export const HoursByStatusReport = defineReport({ name: 'showcase_hours_by_status', label: 'Hours by Status (Summary)', description: 'Estimated hours grouped by task status.', @@ -20,10 +20,10 @@ export const HoursByStatusReport: Report = { dataset: 'showcase_task_metrics', rows: ['status'], values: ['est_hours'], -}; +}); /** 3 ── Matrix: status (down) × priority (across) cross-tab. */ -export const StatusPriorityMatrixReport: Report = { +export const StatusPriorityMatrixReport = defineReport({ name: 'showcase_status_priority_matrix', label: 'Status × Priority (Matrix)', description: 'Task counts cross-tabulated by status and priority.', @@ -34,10 +34,10 @@ export const StatusPriorityMatrixReport: Report = { rows: ['status'], columns: ['priority'], values: ['est_hours'], -}; +}); /** 4 ── Joined: multiple stacked blocks in one report. */ -export const TaskOverviewReport: Report = { +export const TaskOverviewReport = defineReport({ name: 'showcase_task_overview', label: 'Task Overview (Joined)', description: 'Multiple task sub-reports stacked into one joined view.', @@ -66,7 +66,7 @@ export const TaskOverviewReport: Report = { runtimeFilter: { done: true }, }, ], -}; +}); export const allReports = [ HoursByStatusReport, diff --git a/examples/app-todo/src/actions/task.actions.ts b/examples/app-todo/src/actions/task.actions.ts index bf5405cd75..2956f69bcf 100644 --- a/examples/app-todo/src/actions/task.actions.ts +++ b/examples/app-todo/src/actions/task.actions.ts @@ -1,9 +1,9 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Action } from '@objectstack/spec/ui'; +import { defineAction } from '@objectstack/spec/ui'; /** Mark Task as Complete */ -export const CompleteTaskAction: Action = { +export const CompleteTaskAction = defineAction({ name: 'complete_task', label: 'Mark Complete', objectName: 'todo_task', @@ -17,10 +17,10 @@ export const CompleteTaskAction: Action = { exposed: true, description: 'Mark a todo task as complete. Use when the user says a task is done or finished.', }, -}; +}); /** Mark Task as In Progress */ -export const StartTaskAction: Action = { +export const StartTaskAction = defineAction({ name: 'start_task', label: 'Start Task', objectName: 'todo_task', @@ -34,10 +34,10 @@ export const StartTaskAction: Action = { exposed: true, description: 'Mark a todo task as in progress. Use when the user says they are starting or working on a task.', }, -}; +}); /** Defer Task */ -export const DeferTaskAction: Action = { +export const DeferTaskAction = defineAction({ name: 'defer_task', label: 'Defer Task', objectName: 'todo_task', @@ -61,10 +61,10 @@ export const DeferTaskAction: Action = { ], successMessage: 'Task deferred successfully!', refreshAfter: true, -}; +}); /** Set Reminder */ -export const SetReminderAction: Action = { +export const SetReminderAction = defineAction({ name: 'set_reminder', label: 'Set Reminder', objectName: 'todo_task', @@ -82,10 +82,10 @@ export const SetReminderAction: Action = { ], successMessage: 'Reminder set!', refreshAfter: true, -}; +}); /** Clone Task */ -export const CloneTaskAction: Action = { +export const CloneTaskAction = defineAction({ name: 'clone_task', label: 'Clone Task', objectName: 'todo_task', @@ -99,10 +99,10 @@ export const CloneTaskAction: Action = { exposed: true, description: 'Duplicate an existing todo task, copying its fields into a new task record.', }, -}; +}); /** Mass Complete Tasks */ -export const MassCompleteTasksAction: Action = { +export const MassCompleteTasksAction = defineAction({ name: 'mass_complete', label: 'Complete Selected', objectName: 'todo_task', @@ -116,10 +116,10 @@ export const MassCompleteTasksAction: Action = { exposed: true, description: 'Mark all currently selected todo tasks as complete in one bulk operation.', }, -}; +}); /** Delete Completed Tasks */ -export const DeleteCompletedAction: Action = { +export const DeleteCompletedAction = defineAction({ name: 'delete_completed', label: 'Delete Completed', objectName: 'todo_task', @@ -141,10 +141,10 @@ export const DeleteCompletedAction: Action = { // confirmText + variant:'danger' default this to requiring HITL approval; // it registers only when enableActionApproval is on, then routes to the queue. }, -}; +}); /** Export Tasks to CSV */ -export const ExportToCsvAction: Action = { +export const ExportToCsvAction = defineAction({ name: 'export_csv', label: 'Export to CSV', objectName: 'todo_task', @@ -158,4 +158,4 @@ export const ExportToCsvAction: Action = { exposed: true, description: 'Export the current list of todo tasks to a downloadable CSV file.', }, -}; +}); diff --git a/examples/app-todo/src/reports/task.report.ts b/examples/app-todo/src/reports/task.report.ts index 32809df873..d91bf53c11 100644 --- a/examples/app-todo/src/reports/task.report.ts +++ b/examples/app-todo/src/reports/task.report.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { ReportInput } from '@objectstack/spec/ui'; +import { defineReport } from '@objectstack/spec/ui'; // ADR-0021 Phase 2: each report below carries a `task_metrics` dataset binding // (`dataset` + `rows` + `values`, measures referenced BY NAME) alongside the @@ -10,7 +10,7 @@ import type { ReportInput } from '@objectstack/spec/ui'; // drilldown (per the migration decision); `overdue_tasks` becomes a ListView. /** Tasks by Status Report */ -export const TasksByStatusReport: ReportInput = { +export const TasksByStatusReport = defineReport({ name: 'tasks_by_status', label: 'Tasks by Status', description: 'Summary of tasks grouped by status', @@ -18,10 +18,10 @@ export const TasksByStatusReport: ReportInput = { dataset: 'task_metrics', rows: ['status'], values: ['task_count'], -}; +}); /** Tasks by Priority Report */ -export const TasksByPriorityReport: ReportInput = { +export const TasksByPriorityReport = defineReport({ name: 'tasks_by_priority', label: 'Tasks by Priority', description: 'Summary of tasks grouped by priority level', @@ -30,10 +30,10 @@ export const TasksByPriorityReport: ReportInput = { rows: ['priority'], values: ['task_count'], runtimeFilter: { is_completed: false }, -}; +}); /** Tasks by Owner Report */ -export const TasksByOwnerReport: ReportInput = { +export const TasksByOwnerReport = defineReport({ name: 'tasks_by_owner', label: 'Tasks by Owner', description: 'Task summary by assignee', @@ -42,7 +42,7 @@ export const TasksByOwnerReport: ReportInput = { rows: ['owner'], values: ['est_hours', 'actual_hours'], runtimeFilter: { is_completed: false }, -}; +}); // ADR-0021 Phase 2: the former `OverdueTasksReport` (a flat record list, no // grouping/aggregation) is now the `overdue` ListView on todo_task — see @@ -50,7 +50,7 @@ export const TasksByOwnerReport: ReportInput = { // (ADR-0017), not a dataset report. /** Completed Tasks Report */ -export const CompletedTasksReport: ReportInput = { +export const CompletedTasksReport = defineReport({ name: 'completed_tasks', label: 'Completed Tasks', description: 'All completed tasks with time tracking', @@ -59,10 +59,10 @@ export const CompletedTasksReport: ReportInput = { rows: ['category'], values: ['est_hours', 'actual_hours'], runtimeFilter: { is_completed: true }, -}; +}); /** Time Tracking Report */ -export const TimeTrackingReport: ReportInput = { +export const TimeTrackingReport = defineReport({ name: 'time_tracking', label: 'Time Tracking Report', description: 'Estimated vs actual hours analysis', @@ -74,4 +74,4 @@ export const TimeTrackingReport: ReportInput = { rows: ['owner', 'category'], values: ['est_hours', 'actual_hours'], runtimeFilter: { is_completed: true }, -}; +}); diff --git a/examples/app-todo/src/translations/todo.translation.ts b/examples/app-todo/src/translations/todo.translation.ts index 8bc885ced2..9355325246 100644 --- a/examples/app-todo/src/translations/todo.translation.ts +++ b/examples/app-todo/src/translations/todo.translation.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { TranslationBundle } from '@objectstack/spec/system'; +import { defineTranslationBundle } from '@objectstack/spec/system'; import { en } from './en'; import { zhCN } from './zh-CN'; import { jaJP } from './ja-JP'; @@ -17,8 +17,8 @@ import { jaJP } from './ja-JP'; * * Supported locales: en (English), zh-CN (Chinese), ja-JP (Japanese) */ -export const TodoTranslations: TranslationBundle = { +export const TodoTranslations = defineTranslationBundle({ en, 'zh-CN': zhCN, 'ja-JP': jaJP, -}; +}); From 2e8c47a070cbc28ade69bee11bbec3dd0fcc0612 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:38:15 +0800 Subject: [PATCH 3/3] feat(lint): guard examples against bare metadata literals (#2035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an AST-only no-restricted-syntax rule that flags exported consts in examples/** annotated with a spec domain type (simple `Page` or qualified `UI.Page`) instead of being wrapped in the defineX factory. Turns the authoring convention into an invariant so the "pick by history" pattern cannot creep back into the reference corpus AI learns from — it already caught 15 unmigrated authoring files in this change. No type info required. Co-Authored-By: Claude Opus 4.8 --- eslint.config.mjs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 533dbb164a..d05fe59e94 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,6 +31,22 @@ const SUBPATH_RULE_MESSAGE = 're-exports were removed because Node ESM cannot tree-shake them — see ' + 'packages/spec/src/index.ts.'; +// issue #2035 — the 16 writable domains that now have a `defineX` factory. In +// example/app metadata files these must be authored through the factory, never a +// bare `: DomainType` / `: DomainTypeInput` literal: the factory validates at +// `.parse()` time and is a *value* import that fails loudly on a broken import +// instead of silently degrading to `any` (the #2023 failure mode). +const DOMAIN_TYPES = [ + 'Datasource', 'Connector', 'Policy', 'SharingRule', 'Role', 'PermissionSet', + 'EmailTemplateDefinition', 'Report', 'Webhook', 'ObjectExtension', 'Cube', + 'Mapping', 'Theme', 'TranslationBundle', 'Page', 'Action', +].flatMap((t) => [t, t + 'Input']).join('|'); + +const DOMAIN_RULE_MESSAGE = + 'Author this metadata through its defineX factory (e.g. `definePage({ ... })`) ' + + 'instead of a bare `: Type` literal. The factory validates at parse time and a ' + + 'broken value import fails loudly instead of degrading to `any` — see issue #2035.'; + export default [ { files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], @@ -62,4 +78,29 @@ export default [ }], }, }, + // issue #2035 — authoring-entry guard. Flags exported consts in example + // metadata files that are annotated with a spec domain type (simple `Page` + // or qualified `UI.Page`) instead of being wrapped in the `defineX` factory. + // AST-only (no type info): matches the declaration shape, not local vars or + // function params. Scoped to examples — the reference corpus AI learns from. + { + files: ['examples/**/*.{ts,tsx,mts,cts}'], + ignores: ['**/node_modules/**', '**/dist/**'], + languageOptions: { + parser: tsParser, + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + }, + rules: { + 'no-restricted-syntax': ['error', + { + selector: `ExportNamedDeclaration VariableDeclarator[id.typeAnnotation.typeAnnotation.typeName.name=/^(${DOMAIN_TYPES})$/]`, + message: DOMAIN_RULE_MESSAGE, + }, + { + selector: `ExportNamedDeclaration VariableDeclarator[id.typeAnnotation.typeAnnotation.typeName.right.name=/^(${DOMAIN_TYPES})$/]`, + message: DOMAIN_RULE_MESSAGE, + }, + ], + }, + }, ];