Skip to content

Commit ac89550

Browse files
Copilothotlong
andcommitted
Phase 1: Add ExecutionContext schema and context field to all DataEngine options
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 7564b0b commit ac89550

5 files changed

Lines changed: 199 additions & 6 deletions

File tree

packages/spec/src/data/data-engine.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,82 @@ describe('DataEngineRequestSchema', () => {
644644
});
645645

646646
describe('Integration Tests', () => {
647+
it('should support context in query options', () => {
648+
const options = DataEngineQueryOptionsSchema.parse({
649+
filter: { status: 'active' },
650+
context: {
651+
userId: 'user_123',
652+
tenantId: 'org_456',
653+
roles: ['admin'],
654+
},
655+
});
656+
657+
expect(options.context?.userId).toBe('user_123');
658+
expect(options.context?.tenantId).toBe('org_456');
659+
expect(options.context?.roles).toEqual(['admin']);
660+
});
661+
662+
it('should support context in insert options', () => {
663+
const options = DataEngineInsertOptionsSchema.parse({
664+
returning: true,
665+
context: { userId: 'user_1', isSystem: false },
666+
});
667+
668+
expect(options.context?.userId).toBe('user_1');
669+
});
670+
671+
it('should support context in update options', () => {
672+
const options = DataEngineUpdateOptionsSchema.parse({
673+
filter: { id: '1' },
674+
context: { userId: 'user_1', tenantId: 'org_1' },
675+
});
676+
677+
expect(options.context?.tenantId).toBe('org_1');
678+
});
679+
680+
it('should support context in delete options', () => {
681+
const options = DataEngineDeleteOptionsSchema.parse({
682+
filter: { id: '1' },
683+
context: { isSystem: true },
684+
});
685+
686+
expect(options.context?.isSystem).toBe(true);
687+
});
688+
689+
it('should support context in count options', () => {
690+
const options = DataEngineCountOptionsSchema.parse({
691+
filter: { status: 'active' },
692+
context: { userId: 'u1' },
693+
});
694+
695+
expect(options.context?.userId).toBe('u1');
696+
});
697+
698+
it('should support context in aggregate options', () => {
699+
const options = DataEngineAggregateOptionsSchema.parse({
700+
groupBy: ['status'],
701+
context: { userId: 'u1', roles: ['analyst'] },
702+
});
703+
704+
expect(options.context?.roles).toEqual(['analyst']);
705+
});
706+
707+
it('should accept options without context (backward compatible)', () => {
708+
const queryOpts = DataEngineQueryOptionsSchema.parse({ filter: { x: 1 } });
709+
const insertOpts = DataEngineInsertOptionsSchema.parse({ returning: true });
710+
const updateOpts = DataEngineUpdateOptionsSchema.parse({ upsert: true });
711+
const deleteOpts = DataEngineDeleteOptionsSchema.parse({ multi: true });
712+
const countOpts = DataEngineCountOptionsSchema.parse({});
713+
const aggOpts = DataEngineAggregateOptionsSchema.parse({ groupBy: ['a'] });
714+
715+
expect(queryOpts.context).toBeUndefined();
716+
expect(insertOpts.context).toBeUndefined();
717+
expect(updateOpts.context).toBeUndefined();
718+
expect(deleteOpts.context).toBeUndefined();
719+
expect(countOpts.context).toBeUndefined();
720+
expect(aggOpts.context).toBeUndefined();
721+
});
722+
647723
it('should support complete CRUD workflow', () => {
648724
// Create
649725
const insertRequest = DataEngineInsertRequestSchema.parse({

packages/spec/src/data/data-engine.zod.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { z } from 'zod';
44
import { FilterConditionSchema } from './filter.zod';
55
import { SortNodeSchema } from './query.zod';
6+
import { ExecutionContextSchema } from '../kernel/execution-context.zod';
67

78
/**
89
* Data Engine Protocol
@@ -41,11 +42,26 @@ export const DataEngineSortSchema = z.union([
4142
z.array(SortNodeSchema)
4243
]).describe('Sort order definition');
4344

45+
// ==========================================================================
46+
// 1b. Base Engine Options (shared context)
47+
// ==========================================================================
48+
49+
/**
50+
* Base Engine Options
51+
*
52+
* All Data Engine operation options extend this schema to carry
53+
* an optional ExecutionContext for identity, tenant, and transaction propagation.
54+
*/
55+
export const BaseEngineOptionsSchema = z.object({
56+
/** Execution context (identity, tenant, transaction) */
57+
context: ExecutionContextSchema.optional(),
58+
});
59+
4460
// ==========================================================================
4561
// 2. method: FIND
4662
// ==========================================================================
4763

48-
export const DataEngineQueryOptionsSchema = z.object({
64+
export const DataEngineQueryOptionsSchema = BaseEngineOptionsSchema.extend({
4965
/** Filter conditions (WHERE) */
5066
filter: DataEngineFilterSchema.optional(),
5167

@@ -78,7 +94,7 @@ export const DataEngineQueryOptionsSchema = z.object({
7894
// 3. method: INSERT
7995
// ==========================================================================
8096

81-
export const DataEngineInsertOptionsSchema = z.object({
97+
export const DataEngineInsertOptionsSchema = BaseEngineOptionsSchema.extend({
8298
/**
8399
* Return the inserted record(s)?
84100
* Some drivers support RETURNING clause for efficiency.
@@ -91,7 +107,7 @@ export const DataEngineInsertOptionsSchema = z.object({
91107
// 4. method: UPDATE
92108
// ==========================================================================
93109

94-
export const DataEngineUpdateOptionsSchema = z.object({
110+
export const DataEngineUpdateOptionsSchema = BaseEngineOptionsSchema.extend({
95111
/** Filter conditions to identify records to update */
96112
filter: DataEngineFilterSchema.optional(),
97113

@@ -119,7 +135,7 @@ export const DataEngineUpdateOptionsSchema = z.object({
119135
// 5. method: DELETE
120136
// ==========================================================================
121137

122-
export const DataEngineDeleteOptionsSchema = z.object({
138+
export const DataEngineDeleteOptionsSchema = BaseEngineOptionsSchema.extend({
123139
/** Filter conditions to identify records to delete */
124140
filter: DataEngineFilterSchema.optional(),
125141

@@ -135,7 +151,7 @@ export const DataEngineDeleteOptionsSchema = z.object({
135151
// 6. method: AGGREGATE
136152
// ==========================================================================
137153

138-
export const DataEngineAggregateOptionsSchema = z.object({
154+
export const DataEngineAggregateOptionsSchema = BaseEngineOptionsSchema.extend({
139155
/** Filter conditions (WHERE) */
140156
filter: DataEngineFilterSchema.optional(),
141157

@@ -157,7 +173,7 @@ export const DataEngineAggregateOptionsSchema = z.object({
157173
// 7. method: COUNT
158174
// ==========================================================================
159175

160-
export const DataEngineCountOptionsSchema = z.object({
176+
export const DataEngineCountOptionsSchema = BaseEngineOptionsSchema.extend({
161177
/** Filter conditions */
162178
filter: DataEngineFilterSchema.optional(),
163179
}).describe('Options for DataEngine.count operations');
@@ -335,6 +351,7 @@ export const DataEngineRequestSchema = z.discriminatedUnion('method', [
335351

336352
export type DataEngineFilter = z.infer<typeof DataEngineFilterSchema>;
337353
export type DataEngineSort = z.infer<typeof DataEngineSortSchema>;
354+
export type BaseEngineOptions = z.infer<typeof BaseEngineOptionsSchema>;
338355
export type DataEngineQueryOptions = z.infer<typeof DataEngineQueryOptionsSchema>;
339356
export type DataEngineInsertOptions = z.infer<typeof DataEngineInsertOptionsSchema>;
340357
export type DataEngineUpdateOptions = z.infer<typeof DataEngineUpdateOptionsSchema>;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { ExecutionContextSchema } from './execution-context.zod';
3+
4+
describe('ExecutionContextSchema', () => {
5+
it('should accept empty context (all optional)', () => {
6+
const ctx = ExecutionContextSchema.parse({});
7+
expect(ctx.roles).toEqual([]);
8+
expect(ctx.permissions).toEqual([]);
9+
expect(ctx.isSystem).toBe(false);
10+
});
11+
12+
it('should accept full context', () => {
13+
const ctx = ExecutionContextSchema.parse({
14+
userId: 'user_123',
15+
tenantId: 'org_456',
16+
roles: ['admin', 'editor'],
17+
permissions: ['read:account', 'write:account'],
18+
isSystem: false,
19+
accessToken: 'Bearer abc',
20+
traceId: 'trace-789',
21+
});
22+
23+
expect(ctx.userId).toBe('user_123');
24+
expect(ctx.tenantId).toBe('org_456');
25+
expect(ctx.roles).toEqual(['admin', 'editor']);
26+
expect(ctx.permissions).toEqual(['read:account', 'write:account']);
27+
expect(ctx.isSystem).toBe(false);
28+
expect(ctx.accessToken).toBe('Bearer abc');
29+
expect(ctx.traceId).toBe('trace-789');
30+
});
31+
32+
it('should default roles and permissions to empty arrays', () => {
33+
const ctx = ExecutionContextSchema.parse({ userId: 'u1' });
34+
expect(ctx.roles).toEqual([]);
35+
expect(ctx.permissions).toEqual([]);
36+
});
37+
38+
it('should default isSystem to false', () => {
39+
const ctx = ExecutionContextSchema.parse({});
40+
expect(ctx.isSystem).toBe(false);
41+
});
42+
43+
it('should accept system context', () => {
44+
const ctx = ExecutionContextSchema.parse({ isSystem: true });
45+
expect(ctx.isSystem).toBe(true);
46+
});
47+
48+
it('should accept transaction handle', () => {
49+
const mockTx = { id: 'tx1', commit: () => {} };
50+
const ctx = ExecutionContextSchema.parse({ transaction: mockTx });
51+
expect(ctx.transaction).toBeDefined();
52+
});
53+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { z } from 'zod';
4+
5+
/**
6+
* Execution Context Schema
7+
*
8+
* Defines the runtime context that flows from HTTP request → data operations.
9+
* This is the "identity + environment" envelope that every data operation can carry.
10+
*
11+
* Design:
12+
* - All fields are optional for backward compatibility
13+
* - `isSystem` bypasses permission checks (for internal/migration operations)
14+
* - `transaction` carries the database transaction handle for atomicity
15+
* - `traceId` enables distributed tracing across microservices
16+
*
17+
* Usage:
18+
* engine.find('account', { context: { userId: '...', tenantId: '...' } })
19+
*/
20+
export const ExecutionContextSchema = z.object({
21+
/** Current user ID (resolved from session) */
22+
userId: z.string().optional(),
23+
24+
/** Current organization/tenant ID (resolved from session.activeOrganizationId) */
25+
tenantId: z.string().optional(),
26+
27+
/** User role names (resolved from Member + Role) */
28+
roles: z.array(z.string()).default([]),
29+
30+
/** Aggregated permission names (resolved from PermissionSet) */
31+
permissions: z.array(z.string()).default([]),
32+
33+
/** Whether this is a system-level operation (bypasses permission checks) */
34+
isSystem: z.boolean().default(false),
35+
36+
/** Raw access token (for external API call pass-through) */
37+
accessToken: z.string().optional(),
38+
39+
/** Database transaction handle */
40+
transaction: z.unknown().optional(),
41+
42+
/** Request trace ID (for distributed tracing) */
43+
traceId: z.string().optional(),
44+
});
45+
46+
export type ExecutionContext = z.infer<typeof ExecutionContextSchema>;

packages/spec/src/kernel/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export * from './service-registry.zod';
2020
export * from './startup-orchestrator.zod';
2121
export * from './plugin-registry.zod';
2222
export * from './plugin-security.zod';
23+
export * from './execution-context.zod';

0 commit comments

Comments
 (0)