Skip to content

Commit 75090c3

Browse files
committed
objectql 内核改造
1 parent 187e7ae commit 75090c3

2 files changed

Lines changed: 166 additions & 2 deletions

File tree

packages/objectql/src/engine.ts

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export class ObjectQL implements IDataEngine {
8282
fn: EngineMiddleware;
8383
object?: string;
8484
}> = [];
85+
86+
// Action registry: key = "objectName:actionName"
87+
private actions = new Map<string, { handler: (ctx: any) => Promise<any> | any; package?: string }>();
8588

8689
// Host provided context additions (e.g. Server router)
8790
private hostContext: Record<string, any> = {};
@@ -195,6 +198,47 @@ export class ObjectQL implements IDataEngine {
195198
}
196199
}
197200

201+
// ========================================
202+
// Action System
203+
// ========================================
204+
205+
/**
206+
* Register a named action on an object.
207+
* Actions are custom business logic callable via `repo.execute(actionName, params)`.
208+
*
209+
* @param objectName Target object
210+
* @param actionName Unique action name within the object
211+
* @param handler Handler function
212+
* @param packageName Optional package owner (for cleanup)
213+
*/
214+
registerAction(objectName: string, actionName: string, handler: (ctx: any) => Promise<any> | any, packageName?: string): void {
215+
const key = `${objectName}:${actionName}`;
216+
this.actions.set(key, { handler, package: packageName });
217+
this.logger.debug('Registered action', { objectName, actionName, package: packageName });
218+
}
219+
220+
/**
221+
* Execute a named action on an object.
222+
*/
223+
async executeAction(objectName: string, actionName: string, ctx: any): Promise<any> {
224+
const entry = this.actions.get(`${objectName}:${actionName}`);
225+
if (!entry) {
226+
throw new Error(`Action '${actionName}' on object '${objectName}' not found`);
227+
}
228+
return entry.handler(ctx);
229+
}
230+
231+
/**
232+
* Remove all actions registered by a specific package.
233+
*/
234+
removeActionsByPackage(packageName: string): void {
235+
for (const [key, entry] of this.actions.entries()) {
236+
if (entry.package === packageName) {
237+
this.actions.delete(key);
238+
}
239+
}
240+
}
241+
198242
/**
199243
* Register a middleware function
200244
* Middlewares execute in onion model around every data operation.
@@ -930,12 +974,16 @@ export class ObjectQL implements IDataEngine {
930974

931975
/**
932976
* Repository scoped to a single object, bound to an execution context.
977+
*
978+
* Provides both IDataEngine-style methods (find, insert, update, delete)
979+
* and convenience aliases (create, updateById, deleteById) matching
980+
* the @objectql/core ObjectRepository API.
933981
*/
934982
export class ObjectRepository {
935983
constructor(
936984
private objectName: string,
937985
private context: ExecutionContext,
938-
private engine: IDataEngine
986+
private engine: IDataEngine & { executeAction?: (o: string, a: string, c: any) => Promise<any> }
939987
) {}
940988

941989
async find(query: any = {}): Promise<any[]> {
@@ -958,30 +1006,76 @@ export class ObjectRepository {
9581006
});
9591007
}
9601008

1009+
/** Alias for insert() — matches @objectql/core convention */
1010+
async create(data: any): Promise<any> {
1011+
return this.insert(data);
1012+
}
1013+
9611014
async update(data: any, options: any = {}): Promise<any> {
9621015
return this.engine.update(this.objectName, data, {
9631016
...options,
9641017
context: this.context,
9651018
});
9661019
}
9671020

1021+
/** Update a single record by ID */
1022+
async updateById(id: string | number, data: any): Promise<any> {
1023+
return this.engine.update(this.objectName, { ...data, _id: id }, {
1024+
filter: { _id: id },
1025+
context: this.context,
1026+
});
1027+
}
1028+
9681029
async delete(options: any = {}): Promise<any> {
9691030
return this.engine.delete(this.objectName, {
9701031
...options,
9711032
context: this.context,
9721033
});
9731034
}
9741035

1036+
/** Delete a single record by ID */
1037+
async deleteById(id: string | number): Promise<any> {
1038+
return this.engine.delete(this.objectName, {
1039+
filter: { _id: id },
1040+
context: this.context,
1041+
});
1042+
}
1043+
9751044
async count(query: any = {}): Promise<number> {
9761045
return this.engine.count(this.objectName, {
9771046
...query,
9781047
context: this.context,
9791048
});
9801049
}
1050+
1051+
/** Aggregate query */
1052+
async aggregate(query: any = {}): Promise<any[]> {
1053+
return this.engine.aggregate(this.objectName, {
1054+
...query,
1055+
context: this.context,
1056+
});
1057+
}
1058+
1059+
/** Execute a named action registered on this object */
1060+
async execute(actionName: string, params?: any): Promise<any> {
1061+
if (this.engine.executeAction) {
1062+
return this.engine.executeAction(this.objectName, actionName, {
1063+
...params,
1064+
userId: this.context.userId,
1065+
tenantId: this.context.tenantId,
1066+
roles: this.context.roles,
1067+
});
1068+
}
1069+
throw new Error(`Actions not supported by engine`);
1070+
}
9811071
}
9821072

9831073
/**
9841074
* Scoped execution context with object() accessor.
1075+
*
1076+
* Provides identity (userId, tenantId/spaceId, roles),
1077+
* repository access via object(), privilege escalation via sudo(),
1078+
* and transactional execution via transaction().
9851079
*/
9861080
export class ScopedContext {
9871081
constructor(
@@ -991,7 +1085,7 @@ export class ScopedContext {
9911085

9921086
/** Get a repository scoped to this context */
9931087
object(name: string): ObjectRepository {
994-
return new ObjectRepository(name, this.executionContext, this.engine);
1088+
return new ObjectRepository(name, this.executionContext, this.engine as any);
9951089
}
9961090

9971091
/** Create an elevated (system) context */
@@ -1002,7 +1096,54 @@ export class ScopedContext {
10021096
);
10031097
}
10041098

1099+
/**
1100+
* Execute a callback within a database transaction.
1101+
*
1102+
* The callback receives a new ScopedContext whose operations
1103+
* share the same transaction handle. If the callback throws,
1104+
* the transaction is rolled back; otherwise it is committed.
1105+
*
1106+
* Falls back to non-transactional execution if the driver
1107+
* does not support transactions.
1108+
*/
1109+
async transaction(callback: (trxCtx: ScopedContext) => Promise<any>): Promise<any> {
1110+
const engine = this.engine as any;
1111+
1112+
// Find the default driver for transaction support
1113+
const driver = engine.defaultDriver
1114+
? engine.drivers?.get(engine.defaultDriver)
1115+
: undefined;
1116+
1117+
if (!driver?.beginTransaction) {
1118+
// No transaction support — execute directly
1119+
return callback(this);
1120+
}
1121+
1122+
const trx = await driver.beginTransaction();
1123+
const trxCtx = new ScopedContext(
1124+
{ ...this.executionContext, transaction: trx },
1125+
this.engine
1126+
);
1127+
1128+
try {
1129+
const result = await callback(trxCtx);
1130+
if (driver.commit) await driver.commit(trx);
1131+
else if (driver.commitTransaction) await driver.commitTransaction(trx);
1132+
return result;
1133+
} catch (error) {
1134+
if (driver.rollback) await driver.rollback(trx);
1135+
else if (driver.rollbackTransaction) await driver.rollbackTransaction(trx);
1136+
throw error;
1137+
}
1138+
}
1139+
10051140
get userId() { return this.executionContext.userId; }
10061141
get tenantId() { return this.executionContext.tenantId; }
1142+
/** Alias for tenantId — matches ObjectQLContext.spaceId convention */
1143+
get spaceId() { return this.executionContext.tenantId; }
10071144
get roles() { return this.executionContext.roles; }
1145+
get isSystem() { return this.executionContext.isSystem; }
1146+
1147+
/** Internal: expose the transaction handle for driver-level access */
1148+
get transactionHandle() { return this.executionContext.transaction; }
10081149
}

packages/spec/src/data/hook.zod.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,29 @@ export const HookContextSchema = z.object({
178178
* Reference to the ObjectQL engine for performing side effects.
179179
*/
180180
ql: z.unknown().describe('ObjectQL Engine Reference'),
181+
182+
/**
183+
* Cross-Object API
184+
* Provides a scoped data access interface for performing CRUD operations
185+
* on other objects within hooks. Bound to the current execution context
186+
* (userId, tenantId, transaction).
187+
*
188+
* Usage in hooks:
189+
* const users = ctx.api.object('user');
190+
* const admin = await users.findOne({ filter: { role: 'admin' } });
191+
*/
192+
api: z.unknown().optional().describe('Cross-object data access (ScopedContext)'),
193+
194+
/**
195+
* Current User Info
196+
* Convenience shortcut for session.userId + additional user metadata.
197+
* Populated by the engine when available.
198+
*/
199+
user: z.object({
200+
id: z.string().optional(),
201+
name: z.string().optional(),
202+
email: z.string().optional(),
203+
}).optional().describe('Current user info shortcut'),
181204
});
182205

183206
export type Hook = z.input<typeof HookSchema>;

0 commit comments

Comments
 (0)