Skip to content

Commit 98ca40b

Browse files
xuyushun441-sysos-zhuangclaude
authored
test(spec): extend downstream-contract to all 16 writable domains (#2035) (#2095)
The depth gate (#2089) only exercised object/view/action/report/page. Adds a frozen bare-literal fixture for the remaining domains — datasource, connector, policy, sharing rule, role, permission set, email template, webhook, object extension, cube, mapping, theme, translation bundle — typed with the spec's own input aliases and parsed against each domain's schema. Now a narrowed/removed schema property on ANY writable domain breaks the contract (authoring it surfaced two already: EmailTemplate requires `bodyHtml`, Cube requires `dimensions`). 15 cases, typecheck + test green. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 26d7df4 commit 98ca40b

2 files changed

Lines changed: 181 additions & 1 deletion

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// FROZEN bare-literal fixtures for the remaining authoring domains (#2035), so
4+
// the downstream contract exercises the FULL writable surface, not just a few
5+
// domains. Authored the way a third party on a published release did, typed with
6+
// the spec's own input aliases. DO NOT migrate these to the defineX factories
7+
// and DO NOT edit them to make a failing spec change pass — see the README.
8+
import type { DatasourceInput, MappingInput, CubeInput, ObjectExtensionInput } from '@objectstack/spec/data';
9+
import type { ConnectorInput } from '@objectstack/spec/integration';
10+
import type { PolicyInput, SharingRuleInput, PermissionSetInput } from '@objectstack/spec/security';
11+
import type { RoleInput } from '@objectstack/spec/identity';
12+
import type { EmailTemplateDefinitionInput, TranslationBundleInput } from '@objectstack/spec/system';
13+
import type { WebhookInput } from '@objectstack/spec/automation';
14+
import type { ThemeInput } from '@objectstack/spec/ui';
15+
16+
export const DcDatasource: DatasourceInput = {
17+
name: 'dc_primary',
18+
label: 'DC Primary',
19+
driver: 'sqlite',
20+
config: { filename: ':memory:' },
21+
active: true,
22+
};
23+
24+
export const DcConnector: ConnectorInput = {
25+
name: 'dc_hubspot',
26+
label: 'DC HubSpot',
27+
type: 'saas',
28+
description: 'Example SaaS connector.',
29+
authentication: {
30+
type: 'oauth2',
31+
authorizationUrl: 'https://example.com/oauth/authorize',
32+
tokenUrl: 'https://example.com/oauth/token',
33+
clientId: 'env:DC_CLIENT_ID',
34+
clientSecret: 'env:DC_CLIENT_SECRET',
35+
},
36+
actions: [
37+
{
38+
key: 'create_contact',
39+
label: 'Create Contact',
40+
description: 'Create a contact',
41+
inputSchema: { type: 'object', properties: { email: { type: 'string' } }, required: ['email'] },
42+
},
43+
],
44+
};
45+
46+
export const DcPolicy: PolicyInput = {
47+
name: 'dc_default_policy',
48+
isDefault: true,
49+
password: { minLength: 12, requireUppercase: true, requireNumbers: true },
50+
session: { idleTimeout: 30, absoluteTimeout: 480 },
51+
};
52+
53+
export const DcSharingRule: SharingRuleInput = {
54+
type: 'criteria',
55+
name: 'dc_share_customers',
56+
label: 'Customers → managers',
57+
object: 'dc_account',
58+
condition: 'record.stage == "customer"',
59+
accessLevel: 'read',
60+
sharedWith: { type: 'role', value: 'dc_manager' },
61+
active: true,
62+
};
63+
64+
export const DcRole: RoleInput = {
65+
name: 'dc_manager',
66+
label: 'DC Manager',
67+
description: 'Manager role.',
68+
};
69+
70+
export const DcPermissionSet: PermissionSetInput = {
71+
name: 'dc_user',
72+
label: 'DC User',
73+
isProfile: false,
74+
objects: {
75+
dc_account: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
76+
},
77+
};
78+
79+
export const DcEmail: EmailTemplateDefinitionInput = {
80+
name: 'dc.welcome',
81+
label: 'DC Welcome',
82+
category: 'marketing',
83+
locale: 'en-US',
84+
subject: 'Welcome {{contact.first_name}}',
85+
bodyHtml: '<p>Hi {{contact.first_name}}, welcome.</p>',
86+
bodyText: 'Hi {{contact.first_name}}, welcome.',
87+
variables: [{ name: 'contact.first_name', type: 'string', required: true }],
88+
active: true,
89+
};
90+
91+
export const DcWebhook: WebhookInput = {
92+
name: 'dc_account_changed',
93+
label: 'Account Changed',
94+
object: 'dc_account',
95+
triggers: ['create', 'update'],
96+
url: 'https://hooks.example.com/dc/account',
97+
method: 'POST',
98+
isActive: true,
99+
};
100+
101+
export const DcObjectExtension: ObjectExtensionInput = {
102+
extend: 'dc_account',
103+
label: 'Account (extended)',
104+
fields: {
105+
note: { name: 'note', label: 'Note', type: 'text', maxLength: 255 },
106+
},
107+
priority: 210,
108+
};
109+
110+
export const DcCube: CubeInput = {
111+
name: 'dc_pipeline',
112+
title: 'DC Pipeline',
113+
description: 'Account analytics.',
114+
sql: 'dc_account',
115+
measures: {
116+
count: { name: 'count', label: 'Count', type: 'count', sql: '*' },
117+
},
118+
dimensions: {
119+
stage: { name: 'stage', label: 'Stage', type: 'string', sql: 'stage' },
120+
},
121+
};
122+
123+
export const DcMapping: MappingInput = {
124+
name: 'dc_csv_import',
125+
label: 'CSV Import: Accounts',
126+
sourceFormat: 'csv',
127+
targetObject: 'dc_account',
128+
mode: 'upsert',
129+
upsertKey: ['name'],
130+
fieldMapping: [{ source: 'Name', target: 'name', transform: 'none' }],
131+
};
132+
133+
export const DcTheme: ThemeInput = {
134+
name: 'dc_light',
135+
label: 'DC Light',
136+
mode: 'light',
137+
colors: {
138+
primary: '#1E6FD9',
139+
secondary: '#6C757D',
140+
background: '#FFFFFF',
141+
surface: '#F8F9FA',
142+
text: '#212529',
143+
},
144+
};
145+
146+
export const DcTranslationBundle: TranslationBundleInput = {
147+
en: {
148+
objects: { dc_account: { label: 'Account', pluralLabel: 'Accounts' } },
149+
messages: { 'common.save': 'Save' },
150+
},
151+
};

packages/downstream-contract/test/contract.test.ts

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

33
import { describe, it, expect } from 'vitest';
4-
import { ActionSchema, ReportSchema, PageSchema } from '@objectstack/spec/ui';
4+
import { ActionSchema, ReportSchema, PageSchema, ThemeSchema } from '@objectstack/spec/ui';
5+
import { DatasourceSchema, MappingSchema, CubeSchema, ObjectExtensionSchema } from '@objectstack/spec/data';
6+
import { ConnectorSchema } from '@objectstack/spec/integration';
7+
import { PolicySchema, SharingRuleSchema, PermissionSetSchema } from '@objectstack/spec/security';
8+
import { RoleSchema } from '@objectstack/spec/identity';
9+
import { EmailTemplateDefinitionSchema, TranslationBundleSchema } from '@objectstack/spec/system';
10+
import { WebhookSchema } from '@objectstack/spec/automation';
511
import { LogCallAction } from '../src/log-call.action.js';
612
import { AccountsByStageReport } from '../src/pipeline.report.js';
713
import { WelcomePage } from '../src/welcome.page.js';
814
import { ContractStack } from '../src/stack.js';
15+
import * as more from '../src/additional-domains.fixtures.js';
916

1017
// FROZEN — see README. A failure means a spec change dropped or narrowed a
1118
// property a published-spec third party already uses. That is breaking: adjust
@@ -22,4 +29,26 @@ describe('downstream consumer contract (#2035)', () => {
2229
expect(ContractStack.objects).toHaveLength(1);
2330
expect(ContractStack.actions).toHaveLength(2);
2431
});
32+
33+
// Full writable-surface coverage: one frozen bare-literal per remaining domain
34+
// (#2035). A narrowed/removed schema property on ANY of these breaks here.
35+
const cases: Array<[string, { parse: (v: unknown) => unknown }, unknown]> = [
36+
['Datasource', DatasourceSchema, more.DcDatasource],
37+
['Connector', ConnectorSchema, more.DcConnector],
38+
['Policy', PolicySchema, more.DcPolicy],
39+
['SharingRule', SharingRuleSchema, more.DcSharingRule],
40+
['Role', RoleSchema, more.DcRole],
41+
['PermissionSet', PermissionSetSchema, more.DcPermissionSet],
42+
['EmailTemplateDefinition', EmailTemplateDefinitionSchema, more.DcEmail],
43+
['Webhook', WebhookSchema, more.DcWebhook],
44+
['ObjectExtension', ObjectExtensionSchema, more.DcObjectExtension],
45+
['Cube', CubeSchema, more.DcCube],
46+
['Mapping', MappingSchema, more.DcMapping],
47+
['Theme', ThemeSchema, more.DcTheme],
48+
['TranslationBundle', TranslationBundleSchema, more.DcTranslationBundle],
49+
];
50+
51+
it.each(cases)('%s bare-literal parses against the current spec', (_name, schema, fixture) => {
52+
expect(() => schema.parse(fixture)).not.toThrow();
53+
});
2554
});

0 commit comments

Comments
 (0)