Skip to content

Commit 5f875fe

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(spec): defineX factories + XInput aliases for the 16 remaining writable domains (#2035) (#2088)
* feat(spec): defineX factories + XInput aliases for the 16 remaining writable domains (#2035) 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 <noreply@anthropic.com> * refactor(examples): author metadata via defineX factories (#2035) 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 <noreply@anthropic.com> * feat(lint): guard examples against bare metadata literals (#2035) 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 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 94a249f commit 5f875fe

55 files changed

Lines changed: 425 additions & 185 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
spec: add `defineX` factories for the remaining 16 writable domains and the 6
6+
missing `XInput` aliases — one consistent, type-safe authoring entry per domain
7+
(#2035).
8+
9+
New factories: `defineDatasource`, `defineConnector`, `definePolicy`,
10+
`defineSharingRule`, `defineRole`, `definePermissionSet`,
11+
`defineEmailTemplateDefinition`, `defineReport`, `defineWebhook`,
12+
`defineObjectExtension`, `defineCube`, `defineMapping`, `defineTheme`,
13+
`defineTranslationBundle`, `definePage`, `defineAction`. Each mirrors the 19
14+
existing factories (`XSchema.parse(z.input<…>)`): input-shape ergonomics +
15+
authoring-time validation. Because a factory is a *value* import, a broken
16+
import hard-errors instead of silently degrading to `any` (the #2023 failure
17+
mode), and errors surface at `.parse()` time with field-level messages.
18+
19+
Also adds the previously-missing input aliases `PolicyInput`, `CubeInput`,
20+
`MappingInput`, `ThemeInput`, `TranslationBundleInput`, `PageInput`.
21+
22+
Purely additive: no existing exports change.

eslint.config.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ const SUBPATH_RULE_MESSAGE =
3131
're-exports were removed because Node ESM cannot tree-shake them — see ' +
3232
'packages/spec/src/index.ts.';
3333

34+
// issue #2035 — the 16 writable domains that now have a `defineX` factory. In
35+
// example/app metadata files these must be authored through the factory, never a
36+
// bare `: DomainType` / `: DomainTypeInput` literal: the factory validates at
37+
// `.parse()` time and is a *value* import that fails loudly on a broken import
38+
// instead of silently degrading to `any` (the #2023 failure mode).
39+
const DOMAIN_TYPES = [
40+
'Datasource', 'Connector', 'Policy', 'SharingRule', 'Role', 'PermissionSet',
41+
'EmailTemplateDefinition', 'Report', 'Webhook', 'ObjectExtension', 'Cube',
42+
'Mapping', 'Theme', 'TranslationBundle', 'Page', 'Action',
43+
].flatMap((t) => [t, t + 'Input']).join('|');
44+
45+
const DOMAIN_RULE_MESSAGE =
46+
'Author this metadata through its defineX factory (e.g. `definePage({ ... })`) ' +
47+
'instead of a bare `: Type` literal. The factory validates at parse time and a ' +
48+
'broken value import fails loudly instead of degrading to `any` — see issue #2035.';
49+
3450
export default [
3551
{
3652
files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'],
@@ -62,4 +78,29 @@ export default [
6278
}],
6379
},
6480
},
81+
// issue #2035 — authoring-entry guard. Flags exported consts in example
82+
// metadata files that are annotated with a spec domain type (simple `Page`
83+
// or qualified `UI.Page`) instead of being wrapped in the `defineX` factory.
84+
// AST-only (no type info): matches the declaration shape, not local vars or
85+
// function params. Scoped to examples — the reference corpus AI learns from.
86+
{
87+
files: ['examples/**/*.{ts,tsx,mts,cts}'],
88+
ignores: ['**/node_modules/**', '**/dist/**'],
89+
languageOptions: {
90+
parser: tsParser,
91+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
92+
},
93+
rules: {
94+
'no-restricted-syntax': ['error',
95+
{
96+
selector: `ExportNamedDeclaration VariableDeclarator[id.typeAnnotation.typeAnnotation.typeName.name=/^(${DOMAIN_TYPES})$/]`,
97+
message: DOMAIN_RULE_MESSAGE,
98+
},
99+
{
100+
selector: `ExportNamedDeclaration VariableDeclarator[id.typeAnnotation.typeAnnotation.typeName.right.name=/^(${DOMAIN_TYPES})$/]`,
101+
message: DOMAIN_RULE_MESSAGE,
102+
},
103+
],
104+
},
105+
},
65106
];

examples/app-crm/src/actions/convert-lead.action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type * as UI from '@objectstack/spec/ui';
3+
import { defineAction } from '@objectstack/spec/ui';
44

55
/**
66
* Row-level action on crm_lead — launches the Convert Lead screen flow wizard.
77
* Shown as a button in the lead list row menu and in the lead record header.
88
*/
9-
export const ConvertLeadAction: UI.ActionInput = {
9+
export const ConvertLeadAction = defineAction({
1010
name: 'crm_convert_lead',
1111
label: 'Convert Lead',
1212
icon: 'ArrowRightCircle',
@@ -20,4 +20,4 @@ export const ConvertLeadAction: UI.ActionInput = {
2020
// hitting the flow's "already converted" guard screen. The flow keeps that
2121
// guard as a server-side backstop.
2222
visible: 'record.status != "converted"',
23-
};
23+
});

examples/app-crm/src/actions/park-lead.action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type * as UI from '@objectstack/spec/ui';
3+
import { defineAction } from '@objectstack/spec/ui';
44

55
/**
66
* Row-level action on crm_lead — reassign the lead's owner.
@@ -11,7 +11,7 @@ import type * as UI from '@objectstack/spec/ui';
1111
* UndoManager — Ctrl+Z works too). Prompts for the new owner via one param
1212
* (pre-filled with "Triage Queue").
1313
*/
14-
export const ParkLeadAction: UI.ActionInput = {
14+
export const ParkLeadAction = defineAction({
1515
name: 'crm_park_lead',
1616
label: 'Reassign Lead',
1717
icon: 'UserPlus',
@@ -32,4 +32,4 @@ export const ParkLeadAction: UI.ActionInput = {
3232
],
3333
undoable: true,
3434
successMessage: 'Lead reassigned.',
35-
};
35+
});

examples/app-crm/src/analytics/crm.cube.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { Cube } from '@objectstack/spec/data';
3+
import { defineCube } from '@objectstack/spec/data';
44

55
/**
66
* Opportunity Pipeline Cube — revenue metrics broken down by stage,
77
* owner, and account for the CRM sales dashboard.
88
*/
9-
export const PipelineCube: Cube = {
9+
export const PipelineCube = defineCube({
1010
name: 'crm_pipeline',
1111
title: 'CRM Pipeline',
1212
description: 'Revenue and deal-count analytics across the sales pipeline.',
@@ -71,12 +71,12 @@ export const PipelineCube: Cube = {
7171
every: '1 hour',
7272
},
7373
public: false,
74-
};
74+
});
7575

7676
/**
7777
* Lead funnel cube — conversion metrics from lead to opportunity.
7878
*/
79-
export const LeadFunnelCube: Cube = {
79+
export const LeadFunnelCube = defineCube({
8080
name: 'crm_lead_funnel',
8181
title: 'CRM Lead Funnel',
8282
description: 'Lead volume and conversion rate analytics.',
@@ -119,4 +119,4 @@ export const LeadFunnelCube: Cube = {
119119
every: '30 minutes',
120120
},
121121
public: false,
122-
};
122+
});

examples/app-crm/src/connectors/crm-connectors.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { ConnectorInput } from '@objectstack/spec/integration';
3+
import { defineConnector } from '@objectstack/spec/integration';
44

55
/**
66
* HubSpot connector — sync contacts and deals bi-directionally.
77
* Uses OAuth2 for authentication; actual credentials come from environment.
88
*/
9-
export const HubSpotConnector: ConnectorInput = {
9+
export const HubSpotConnector = defineConnector({
1010
name: 'hubspot_crm',
1111
label: 'HubSpot CRM',
1212
type: 'saas',
@@ -86,12 +86,12 @@ export const HubSpotConnector: ConnectorInput = {
8686
requestTimeoutMs: 30000,
8787
status: 'inactive',
8888
enabled: true,
89-
};
89+
});
9090

9191
/**
9292
* Slack connector — post notifications to channels.
9393
*/
94-
export const SlackConnector: ConnectorInput = {
94+
export const SlackConnector = defineConnector({
9595
name: 'slack_notifications',
9696
label: 'Slack',
9797
type: 'api',
@@ -123,4 +123,4 @@ export const SlackConnector: ConnectorInput = {
123123
},
124124
status: 'inactive',
125125
enabled: true,
126-
};
126+
});

examples/app-crm/src/data/crm-mappings.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { Mapping } from '@objectstack/spec/data';
3+
import { defineMapping } from '@objectstack/spec/data';
44

55
/**
66
* CSV import mapping for bulk lead upload.
77
* Maps common CRM export column names to crm_lead fields.
88
*/
9-
export const LeadCsvImportMapping: Mapping = {
9+
export const LeadCsvImportMapping = defineMapping({
1010
name: 'csv_import_leads',
1111
label: 'CSV Import: Leads',
1212
sourceFormat: 'csv',
@@ -51,12 +51,12 @@ export const LeadCsvImportMapping: Mapping = {
5151
],
5252
errorPolicy: 'skip',
5353
batchSize: 500,
54-
};
54+
});
5555

5656
/**
5757
* JSON import mapping for contact sync from external systems (HubSpot, etc.).
5858
*/
59-
export const ContactJsonSyncMapping: Mapping = {
59+
export const ContactJsonSyncMapping = defineMapping({
6060
name: 'json_sync_contacts',
6161
label: 'JSON Sync: Contacts from HubSpot',
6262
sourceFormat: 'json',
@@ -72,4 +72,4 @@ export const ContactJsonSyncMapping: Mapping = {
7272
],
7373
errorPolicy: 'skip',
7474
batchSize: 250,
75-
};
75+
});
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { DatasourceInput } from '@objectstack/spec/data';
3+
import { defineDatasource } from '@objectstack/spec/data';
44

55
/**
66
* Primary CRM datasource — in-memory SQLite for the example.
77
* In production, swap `driver` to 'postgres' and supply real `config`.
88
*/
9-
export const CrmDatasource: DatasourceInput = {
9+
export const CrmDatasource = defineDatasource({
1010
name: 'crm_primary',
1111
label: 'CRM Primary Database',
1212
driver: 'sqlite',
@@ -18,12 +18,12 @@ export const CrmDatasource: DatasourceInput = {
1818
max: 5,
1919
},
2020
active: true,
21-
};
21+
});
2222

2323
/**
2424
* Read-replica for analytics queries — demonstrates datasource routing.
2525
*/
26-
export const CrmAnalyticsDatasource: DatasourceInput = {
26+
export const CrmAnalyticsDatasource = defineDatasource({
2727
name: 'crm_analytics',
2828
label: 'CRM Analytics Read Replica',
2929
driver: 'sqlite',
@@ -32,4 +32,4 @@ export const CrmAnalyticsDatasource: DatasourceInput = {
3232
readOnly: true,
3333
},
3434
active: true,
35-
};
35+
});

examples/app-crm/src/emails/deal-won.email.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { EmailTemplateDefinitionInput } from '@objectstack/spec/system';
3+
import { defineEmailTemplateDefinition } from '@objectstack/spec/system';
44

55
/**
66
* Sent when an opportunity moves to Closed Won.
77
* Referenced by the `notify_owner_deal_won` workflow action.
88
*/
9-
export const DealWonEmail: EmailTemplateDefinitionInput = {
9+
export const DealWonEmail = defineEmailTemplateDefinition({
1010
name: 'crm.deal_won',
1111
label: 'Deal Won — Owner Congrats',
1212
category: 'workflow',
@@ -33,4 +33,4 @@ Nice work!`,
3333
],
3434
active: true,
3535
description: 'Internal congrats email fired by the High-Value Deal workflow when stage = Closed Won.',
36-
};
36+
});

examples/app-crm/src/emails/lead-follow-up.email.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import type { EmailTemplateDefinitionInput } from '@objectstack/spec/system';
3+
import { defineEmailTemplateDefinition } from '@objectstack/spec/system';
44

55
/**
66
* Follow-up nudge sent by the Stale Opportunity workflow when no
77
* activity has been logged on a Lead for the configured threshold.
88
*/
9-
export const LeadFollowUpEmail: EmailTemplateDefinitionInput = {
9+
export const LeadFollowUpEmail = defineEmailTemplateDefinition({
1010
name: 'crm.lead_followup',
1111
label: 'Lead — Follow-Up Reminder',
1212
category: 'notification',
@@ -35,4 +35,4 @@ Follow up: {{lead_url}}`,
3535
],
3636
active: true,
3737
description: 'Internal reminder fired by the Stale Opportunity workflow.',
38-
};
38+
});

0 commit comments

Comments
 (0)