Skip to content

Commit 013062f

Browse files
committed
Refactor ObjectRegistry to MetadataRegistry and enhance plugin loader
Renames ObjectRegistry to MetadataRegistry throughout the codebase for clarity and consistency. Introduces a new LoaderPlugin interface and related types, allowing plugins to register custom metadata loaders. Updates documentation to provide comprehensive guidance on plugin creation, loading, and capabilities, including new loader extension points.
1 parent 2e4a5e6 commit 013062f

11 files changed

Lines changed: 211 additions & 108 deletions

File tree

docs/guide/plugins.md

Lines changed: 163 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,219 @@
11
# Plugin System
22

3-
Plugins allow you to extend the core functionality of ObjectQL by intercepting lifecycle events, modifying metadata, or injecting new services.
3+
Plugins are the primary way to extend ObjectQL. They allow you to bundle behavior, schema modifications, and logic (hooks/actions) into reusable units.
44

5-
## The Plugin Interface
5+
## 1. Anatomy of a Plugin
66

7-
A plugin is simply a class (or object) implementing `ObjectQLPlugin`.
7+
A plugin is simply an object that implements the `ObjectQLPlugin` interface.
88

99
```typescript
1010
import { IObjectQL } from '@objectql/types';
1111

1212
export interface ObjectQLPlugin {
13+
/**
14+
* The unique name of the plugin
15+
*/
1316
name: string;
17+
18+
/**
19+
* Called during initialization, before drivers connect.
20+
* @param app The ObjectQL instance
21+
*/
1422
setup(app: IObjectQL): void | Promise<void>;
1523
}
1624
```
1725

18-
The `setup` method is called during `db.init()`, **before** the database drivers are initialized. This gives plugins a chance to modify the schema metadata.
26+
## 2. Creating Plugins
1927

20-
## Capabilities
28+
You can define plugins in two styles: **Object** or **Class**.
2129

22-
1. **Metadata Mutation**: Modify `app.metadata` to inject fields or create objects dynamically.
23-
2. **Global Hooks**: Use `app.on()` to listen to events on *all* objects.
24-
3. **Action Registry**: Register new actions via `app.registerAction()`.
30+
### Option A: Object Style (Simpler)
31+
Useful for stateless, simple logic or one-off modifications.
2532

26-
## Example: Soft Delete Plugin
33+
```typescript
34+
const MySimplePlugin = {
35+
name: 'my-simple-plugin',
36+
setup(app) {
37+
app.on('before:create', 'user', async (ctx) => {
38+
console.log('Creating user...');
39+
});
40+
}
41+
};
42+
```
2743

28-
This plugin automatically handles "Soft Delete" logic:
29-
1. Injects an `isDeleted` field to all objects.
30-
2. Intercepts `delete` operations to perform an update instead.
31-
3. Intercepts `find` operations to filter out deleted records.
44+
### Option B: Class Style (Recommended)
45+
Useful when your plugin needs to maintain internal state, configuration, or complex initialization logic.
3246

3347
```typescript
34-
import { ObjectQLPlugin, IObjectQL } from '@objectql/types';
48+
class MyComplexPlugin implements ObjectQLPlugin {
49+
name = 'my-complex-plugin';
50+
private config;
3551

36-
export class SoftDeletePlugin implements ObjectQLPlugin {
37-
name = 'soft-delete';
38-
39-
setup(app: IObjectQL) {
40-
// 1. Inject 'isDeleted' field
41-
const objects = app.metadata.list('object');
42-
for (const obj of objects) {
43-
if (!obj.fields.isDeleted) {
44-
obj.fields.isDeleted = { type: 'boolean', default: false };
45-
}
46-
}
52+
constructor(config = {}) {
53+
this.config = config;
54+
}
4755

48-
// 2. Intercept DELETE -> UPDATE
49-
app.on('before:delete', '*', async (ctx) => {
50-
// Prevent actual deletion
51-
ctx.preventDefault();
52-
53-
// Execute internal update
54-
// We use a custom action or system updated to bypass recursion if needed
55-
await app.executeAction(ctx.objectName, 'internalUpdate', {
56-
id: ctx.id,
57-
isDeleted: true
56+
async setup(app: IObjectQL) {
57+
// Access config here
58+
if (this.config.enableLogging) {
59+
app.on('before:*', '*', async (ctx) => {
60+
console.log(`[${ctx.event}] ${ctx.objectName}`);
5861
});
59-
});
60-
61-
// 3. Intercept FIND -> Filter
62-
app.on('before:find', '*', async (ctx) => {
63-
if (ctx.query) {
64-
ctx.query.filters = {
65-
...(ctx.query.filters || {}),
66-
isDeleted: false
67-
}
68-
}
69-
});
62+
}
7063
}
7164
}
7265
```
7366

74-
## Usage
67+
## 3. Loading Plugins
7568

76-
Plugins can be loaded in two ways: by instance or by package name.
69+
Plugins are passed to the `ObjectQL` constructor via the `plugins` array. The loader is very flexible.
7770

78-
### 1. By Instance (Local Development)
71+
### Method 1: Inline Instance
72+
Pass the plugin object or class instance directly.
7973

8074
```typescript
8175
const db = new ObjectQL({
82-
connection: 'sqlite://data.db',
8376
plugins: [
84-
new SoftDeletePlugin()
77+
MySimplePlugin, // Object
78+
new MyComplexPlugin({}) // Class Instance
8579
]
8680
});
8781
```
8882

89-
### 2. By Package Name (Distribution)
90-
91-
If you have installed a plugin via npm (e.g. `npm install @objectql/plugin-audit`), you can simply pass its name. ObjectQL will automatically resolve and instantiate it.
83+
### Method 2: NPM Package (String)
84+
You can specify the package name as a string. ObjectQL uses `require()` (Node.js) to resolve it.
9285

9386
```typescript
9487
const db = new ObjectQL({
95-
connection: 'sqlite://data.db',
9688
plugins: [
97-
'@objectql/plugin-audit'
89+
'@objectql/plugin-audit', // Searches node_modules
90+
'./local-plugins/my-plugin' // Relative path
9891
]
9992
});
10093
```
10194

102-
## Creating a Plugin Package
95+
#### Package Resolution Rules
96+
When loading from a string, ObjectQL tries to find the plugin in the exported module in this order:
10397

104-
To publish a plugin as an npm package:
98+
1. **Class Constructor**: If the module exports a Class (default or module.exports), it tries to instantiation it (`new Plugin()`).
99+
2. **Plugin Object**: If the module exports an object with a `setup` function, it uses it directly.
100+
3. **Default Export**: If the module has a `default` export, it checks that recursively.
105101

106-
1. Create a project exporting your plugin class/instance as the `default` export (or named export).
107-
2. Ensure it implements `ObjectQLPlugin`.
102+
**Example Package (`node_modules/my-plugin/index.js`):**
103+
```javascript
104+
// This works
105+
module.exports = class MyPlugin { ... }
108106

109-
**index.ts**
110-
```typescript
111-
import { ObjectQLPlugin, IObjectQL } from '@objectql/types';
107+
// This also works
108+
module.exports = { name: '..', setup: () => {} }
109+
110+
// This also works (ESM/TS)
111+
export default class MyPlugin { ... }
112+
```
113+
114+
## 4. What can Plugins do?
112115

113-
export default class MyPlugin implements ObjectQLPlugin {
114-
name = 'my-plugin';
115-
setup(app: IObjectQL) {
116-
// ...
116+
The `setup(app)` method gives you full access to the `ObjectQL` instance.
117+
118+
### A. Manipulate Metadata (Schema)
119+
Plugins have full control over the schema. You can modify existing objects or register new ones.
120+
121+
**Example 1: Injecting a global field**
122+
```typescript
123+
setup(app) {
124+
const allObjects = app.metadata.list('object');
125+
for (const obj of allObjects) {
126+
// Add 'createdAt' to every object if missing
127+
if (!obj.fields.createdAt) {
128+
obj.fields.createdAt = { type: 'datetime' };
129+
}
117130
}
118131
}
119132
```
120133

121-
Then in another project:
122-
```bash
123-
npm install my-plugin-package
134+
**Example 2: Registering a new Object**
135+
Plugins can bundle their own data models (e.g. an audit log table).
136+
137+
```typescript
138+
setup(app) {
139+
app.registerObject({
140+
name: 'audit_log',
141+
fields: {
142+
action: { type: 'string' },
143+
userId: { type: 'string' },
144+
timestamp: { type: 'datetime' }
145+
}
146+
});
147+
}
124148
```
149+
150+
**Example 3: Scanning a Directory**
151+
Plugins can also scan a directory to load `*.object.yml` files, just like the main application. This is useful for bundling a set of objects.
152+
153+
```typescript
154+
import * as path from 'path';
155+
156+
setup(app) {
157+
// Scan the 'objects' folder inside the plugin directory
158+
const objectsDir = path.join(__dirname, 'objects');
159+
app.loadFromDirectory(objectsDir);
160+
}
161+
```
162+
163+
### B. Register Global Hooks
164+
Listen to lifecycle events on specific objects or `*` (wildcard).
165+
166+
```typescript
167+
setup(app) {
168+
app.on('before:delete', '*', async (ctx) => {
169+
if (ctx.objectName === 'system_log') {
170+
throw new Error("Logs cannot be deleted");
171+
}
172+
});
173+
}
174+
```
175+
176+
### C. Register Custom Actions
177+
Add new capabilities to objects.
178+
125179
```typescript
126-
// objectql.config.ts
127-
plugins: ['my-plugin-package']
180+
setup(app) {
181+
// Usage: objectql.executeAction('user', 'sendEmail', { ... })
182+
app.registerAction('user', 'sendEmail', async (ctx) => {
183+
await emailService.send(ctx.args.to, ctx.args.body);
184+
});
185+
}
128186
```
187+
188+
### D. Custom Metadata Loaders
189+
Plugins can register new loaders to scan for custom file types (e.g. `*.workflow.yml`). This allows ObjectQL to act as a unified metadata engine.
190+
191+
```typescript
192+
import * as yaml from 'js-yaml';
193+
194+
setup(app) {
195+
app.addLoader({
196+
name: 'workflow-loader',
197+
glob: ['**/*.workflow.yml'],
198+
handler: (ctx) => {
199+
const doc = yaml.load(ctx.content);
200+
const workflowName = doc.name;
201+
202+
// Register into MetadataRegistry with a custom type
203+
ctx.registry.register('workflow', {
204+
type: 'workflow',
205+
id: workflowName,
206+
path: ctx.file,
207+
content: doc
208+
});
209+
}
210+
});
211+
}
212+
```
213+
214+
## 5. Scope Isolation
215+
216+
217+
When a plugin is loaded via **Package Name** (Method 2), ObjectQL automatically marks the hooks and actions registered by that plugin with its package name.
218+
219+
This allows `app.removePackage('@objectql/plugin-auth')` to cleanly remove all hooks and actions associated with that plugin, without affecting others.

packages/core/src/action.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ActionContext, ActionHandler, ObjectRegistry } from '@objectql/types';
1+
import { ActionContext, ActionHandler, MetadataRegistry } from '@objectql/types';
22

33
export interface ActionEntry {
44
handler: ActionHandler;
@@ -17,7 +17,7 @@ export function registerActionHelper(
1717
}
1818

1919
export async function executeActionHelper(
20-
metadata: ObjectRegistry,
20+
metadata: MetadataRegistry,
2121
runtimeActions: Record<string, ActionEntry>,
2222
objectName: string,
2323
actionName: string,

packages/core/src/app.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
ObjectRegistry,
2+
MetadataRegistry,
33
Driver,
44
ObjectConfig,
55
ObjectQLContext,
@@ -11,7 +11,8 @@ import {
1111
HookHandler,
1212
HookContext,
1313
ActionHandler,
14-
ActionContext
14+
ActionContext,
15+
LoaderPlugin
1516
} from '@objectql/types';
1617
import { ObjectLoader } from './loader';
1718
import { ObjectRepository } from './repository';
@@ -23,7 +24,7 @@ import { registerHookHelper, triggerHookHelper, HookEntry } from './hook';
2324
import { registerObjectHelper, getConfigsHelper } from './object';
2425

2526
export class ObjectQL implements IObjectQL {
26-
public metadata: ObjectRegistry;
27+
public metadata: MetadataRegistry;
2728
private loader: ObjectLoader;
2829
private datasources: Record<string, Driver> = {};
2930
private remotes: string[] = [];
@@ -32,7 +33,7 @@ export class ObjectQL implements IObjectQL {
3233
private pluginsList: ObjectQLPlugin[] = [];
3334

3435
constructor(config: ObjectQLConfig) {
35-
this.metadata = config.registry || new ObjectRegistry();
36+
this.metadata = config.registry || new MetadataRegistry();
3637
this.loader = new ObjectLoader(this.metadata);
3738
this.datasources = config.datasources || {};
3839
this.remotes = config.remotes || [];
@@ -123,6 +124,10 @@ export class ObjectQL implements IObjectQL {
123124
this.loader.load(dir, packageName);
124125
}
125126

127+
addLoader(plugin: LoaderPlugin) {
128+
this.loader.use(plugin);
129+
}
130+
126131
createContext(options: ObjectQLContextOptions): ObjectQLContext {
127132
const ctx: ObjectQLContext = {
128133
userId: options.userId,

packages/core/src/hook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HookContext, HookHandler, HookName, ObjectRegistry } from '@objectql/types';
1+
import { HookContext, HookHandler, HookName, MetadataRegistry } from '@objectql/types';
22

33
export interface HookEntry {
44
objectName: string;
@@ -20,7 +20,7 @@ export function registerHookHelper(
2020
}
2121

2222
export async function triggerHookHelper(
23-
metadata: ObjectRegistry,
23+
metadata: MetadataRegistry,
2424
runtimeHooks: Record<string, HookEntry[]>,
2525
event: HookName,
2626
objectName: string,

0 commit comments

Comments
 (0)