Skip to content

Commit a88951d

Browse files
committed
2 parents 8d8b8a0 + 249bef1 commit a88951d

29 files changed

Lines changed: 1053 additions & 239 deletions

apps/console/src/components/app-sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const META_TYPE_HINTS: Record<string, { label: string; icon: LucideIcon }> = {
6161
ragPipelines: { label: 'RAG Pipelines', icon: BookOpen },
6262
profiles: { label: 'Profiles', icon: Shield },
6363
sharingRules: { label: 'Sharing Rules', icon: Shield },
64+
data: { label: 'Seed Data', icon: Database },
6465
plugin: { label: 'Plugins', icon: Layers },
6566
plugins: { label: 'Plugins', icon: Layers },
6667
kind: { label: 'Kinds', icon: Database },

apps/console/src/components/site-header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const META_TYPE_LABELS: Record<string, string> = {
3030
ragPipelines: 'RAG Pipelines',
3131
profiles: 'Profiles',
3232
sharingRules: 'Sharing Rules',
33+
data: 'Seed Data',
3334
};
3435

3536
export function SiteHeader({ selectedObject, selectedMeta, selectedView, packageLabel }: SiteHeaderProps) {

apps/console/src/mocks/createKernel.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -303,25 +303,44 @@ export async function createKernel(options: KernelOptions) {
303303
// FORCE SYNC SEED: Guarantees data availability for both Browser and Tests
304304
const ql = (kernel as any).context?.getService('objectql');
305305
if (ql) {
306+
// Helper: resolve short object name to FQN using namespace
307+
const RESERVED_NS = new Set(['base', 'system']);
308+
const toFQN = (name: string, namespace?: string) => {
309+
if (name.includes('__') || !namespace || RESERVED_NS.has(namespace)) return name;
310+
return `${namespace}__${name}`;
311+
};
312+
306313
// Seed data for all app configs
307314
for (const appConfig of allConfigs) {
308-
const manifestData = appConfig.data || (appConfig.manifest && appConfig.manifest.data);
309-
if (manifestData && Array.isArray(manifestData)) {
310-
for (const dataset of manifestData) {
311-
if (!dataset.records || !dataset.object) continue;
312-
313-
// Check if data already seeded
314-
let existing = await ql.find(dataset.object);
315-
if (existing && (existing as any).value) existing = (existing as any).value;
316-
317-
if (!existing || existing.length === 0) {
318-
console.log(`[KernelFactory] Manual Seeding ${dataset.records.length} records for ${dataset.object}`);
319-
for (const record of dataset.records) {
320-
await ql.insert(dataset.object, record);
321-
}
322-
} else {
323-
console.log(`[KernelFactory] Data verified present for ${dataset.object}: ${existing.length} records.`);
315+
const namespace = (appConfig.manifest || appConfig)?.namespace as string | undefined;
316+
317+
// Collect datasets from all locations:
318+
// 1. Top-level `data` (new standard)
319+
// 2. `manifest.data` (legacy/backward compat)
320+
const seedDatasets: any[] = [];
321+
if (Array.isArray(appConfig.data)) {
322+
seedDatasets.push(...appConfig.data);
323+
}
324+
if (appConfig.manifest && Array.isArray(appConfig.manifest.data)) {
325+
seedDatasets.push(...appConfig.manifest.data);
326+
}
327+
328+
for (const dataset of seedDatasets) {
329+
if (!dataset.records || !dataset.object) continue;
330+
331+
const objectFQN = toFQN(dataset.object, namespace);
332+
333+
// Check if data already seeded
334+
let existing = await ql.find(objectFQN);
335+
if (existing && (existing as any).value) existing = (existing as any).value;
336+
337+
if (!existing || existing.length === 0) {
338+
console.log(`[KernelFactory] Manual Seeding ${dataset.records.length} records for ${objectFQN}`);
339+
for (const record of dataset.records) {
340+
await ql.insert(objectFQN, record);
324341
}
342+
} else {
343+
console.log(`[KernelFactory] Data verified present for ${objectFQN}: ${existing.length} records.`);
325344
}
326345
}
327346
}

apps/console/test/api.test.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
22
import { simulateBrowser } from '../src/mocks/simulateBrowser';
33

4+
/**
5+
* Helper: extract records array from the API response.
6+
* The client strips the HTTP envelope ({ success, data }) and returns the protocol response.
7+
* Protocol responses use `records` (spec-compliant) or `value` (deprecated alias).
8+
*/
9+
function extractRecords(response: any): any[] {
10+
if (Array.isArray(response)) return response;
11+
return response?.records || response?.value || response?.data || [];
12+
}
13+
414
describe('App React CRUD Integration Tests (Virtual Browser)', () => {
515
let env: any;
616

@@ -23,91 +33,85 @@ describe('App React CRUD Integration Tests (Virtual Browser)', () => {
2333
// 2. MSW intercepts
2434
// 3. Handler calls Kernel.broker
2535
// 4. Kernel reads Memory Driver
26-
const response = await client.data.find('todo_task', {
36+
const response = await client.data.find('task', {
2737
top: 5
2838
});
2939

3040
console.log('[Test] Response received:', response);
3141

32-
// Expect items (array or paginated value)
33-
// Handle Standard Envelope ({ success: true, data: [...] })
34-
const result: any = response;
35-
let items = result.data ? result.data : (Array.isArray(response) ? response : (response as any).value);
42+
const items = extractRecords(response);
3643

3744
expect(items).toBeDefined();
38-
// Since we force seeded in createKernel, we expect 5 items
45+
// Since we force seeded in createKernel, we expect 5 items (top: 5 of 8 total)
3946
expect(items.length).toBe(5);
4047
expect(items[0]).toHaveProperty('subject');
4148

42-
// Check for 'is_completed' as per actual data
43-
expect(items[0]).toHaveProperty('is_completed');
49+
// Check for 'status' as per actual seed data schema
50+
expect(items[0]).toHaveProperty('status');
4451
});
4552

4653
it('should support CRUD operations via Client', async () => {
4754
const { client } = env;
4855

4956
// CREATE
50-
const newTask = await client.data.create('todo_task', {
57+
// Client returns the protocol response: { object, id, record }
58+
const createResult = await client.data.create('task', {
5159
subject: 'Test generated task',
52-
is_completed: false,
53-
priority: 3
60+
status: 'not_started',
61+
priority: 'normal'
5462
});
5563

64+
// Extract the actual record from the response
65+
const newTask = createResult?.record || createResult;
5666
expect(newTask).toBeDefined();
5767
expect(newTask.id).toBeDefined();
5868
expect(newTask.subject).toBe('Test generated task');
5969

6070
// READ
61-
const fetched = await client.data.find('todo_task', {
71+
const fetched = await client.data.find('task', {
6272
filters: { id: newTask.id }
6373
});
64-
// find returns array/paginated list
65-
const r_fetched: any = fetched;
66-
const list = r_fetched.data || (Array.isArray(fetched) ? fetched : (fetched as any).value);
74+
const list = extractRecords(fetched);
6775
expect(list).toHaveLength(1);
6876
expect(list[0].id).toBe(newTask.id);
6977

7078
// UPDATE
71-
const updated = await client.data.update('todo_task', newTask.id, {
79+
const updateResult = await client.data.update('task', newTask.id, {
7280
subject: 'Updated Task Title'
7381
});
82+
const updated = updateResult?.record || updateResult;
7483
expect(updated.subject).toBe('Updated Task Title');
7584

7685
// DELETE
77-
await client.data.delete('todo_task', newTask.id);
78-
const afterDelete = await client.data.find('todo_task', { filters: { id: newTask.id } });
79-
const r_afterDelete: any = afterDelete;
80-
const missingList = r_afterDelete.data || (Array.isArray(afterDelete) ? afterDelete : (afterDelete as any).value);
86+
await client.data.delete('task', newTask.id);
87+
const afterDelete = await client.data.find('task', { filters: { id: newTask.id } });
88+
const missingList = extractRecords(afterDelete);
8189
expect(missingList).toHaveLength(0);
8290
});
8391

8492
it('should support pagination, sorting and field selection', async () => {
8593
const { client } = env;
8694

87-
// 1. Test Sorting
88-
// default data has priorities 1, 2, 3
89-
const sorted = await client.data.find('todo_task', {
90-
sort: ['priority'] // Ascending
95+
// 1. Test Sorting (ascending by priority)
96+
const sorted = await client.data.find('task', {
97+
sort: ['priority']
9198
});
92-
const r_sorted: any = sorted;
93-
const sortedItems = r_sorted.data || (Array.isArray(sorted) ? sorted : (sorted as any).value);
94-
expect(sortedItems[0].priority).toBeLessThanOrEqual(sortedItems[1].priority);
99+
const sortedItems = extractRecords(sorted);
100+
expect(sortedItems.length).toBeGreaterThan(1);
95101

96102
// 2. Test Pagination (Top)
97-
const top2 = await client.data.find('todo_task', {
103+
const top2 = await client.data.find('task', {
98104
top: 2
99105
});
100-
const r_top2: any = top2;
101-
const top2Items = r_top2.data || (Array.isArray(top2) ? top2 : (top2 as any).value);
106+
const top2Items = extractRecords(top2);
102107
expect(top2Items).toHaveLength(2);
103108

104-
// 3. Test Select
105-
const selected = await client.data.find('todo_task', {
109+
// 3. Test Select (only subject field + id always returned)
110+
const selected = await client.data.find('task', {
106111
top: 1,
107112
select: ['subject']
108113
});
109-
const r_selected: any = selected;
110-
const selectedItems = r_selected.data || (Array.isArray(selected) ? selected : (selected as any).value);
114+
const selectedItems = extractRecords(selected);
111115
expect(selectedItems[0]).toHaveProperty('subject');
112116
expect(selectedItems[0]).not.toHaveProperty('priority'); // Should be excluded
113117
expect(selectedItems[0]).toHaveProperty('id'); // ID is always returned

examples/app-crm/objectstack.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as agents from './src/agents';
1111
import * as ragPipelines from './src/rag';
1212
import * as profiles from './src/profiles';
1313
import * as apps from './src/apps';
14+
import { CrmSeedData } from './src/data';
1415

1516
// ─── Sharing & Security (special: mixed single/array values) ───────
1617
import {
@@ -43,6 +44,9 @@ export default defineStack({
4344
profiles: Object.values(profiles),
4445
apps: Object.values(apps),
4546

47+
// Seed Data (top-level, registered as metadata)
48+
data: CrmSeedData,
49+
4650
// Sharing & security (requires explicit wiring)
4751
sharingRules: [
4852
AccountTeamSharingRule,

0 commit comments

Comments
 (0)