Skip to content

Commit aa477ee

Browse files
committed
Add plugin system and update configuration options
Introduces a plugin system to ObjectQL, allowing plugins to extend core functionality via lifecycle hooks. Updates configuration to support connection strings, multiple schema sources, presets, and plugins. Adds documentation for configuration and plugins, and updates example and type definitions accordingly.
1 parent 8a698ae commit aa477ee

6 files changed

Lines changed: 206 additions & 34 deletions

File tree

docs/guide/configuration.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Configuration
2+
3+
ObjectQL is configured by passing an `ObjectQLConfig` object to the constructor.
4+
5+
## Basic Layout
6+
7+
```typescript
8+
// objectql.config.ts
9+
import { ObjectQL } from '@objectql/core';
10+
11+
export const db = new ObjectQL({
12+
connection: 'sqlite://data.db', // 1. Infrastructure
13+
presets: ['@objectql/preset-auth'], // 2. Base Capabilities
14+
source: ['src'], // 3. Application Logic
15+
plugins: [] // 4. Extensions
16+
});
17+
```
18+
19+
## Reference
20+
21+
### `connection` (string)
22+
The Connection String URI defining the database connection.
23+
* `sqlite://path/to/db`
24+
* `postgres://user:pass@host:5432/db`
25+
* `mongodb://host:27017/db`
26+
27+
The engine will automatically load the appropriate driver (`@objectql/driver-knex` or `@objectql/driver-mongo`).
28+
29+
### `source` (string | string[])
30+
One or more directory paths (relative or absolute) containing your schema files (`*.object.yml`).
31+
The loader scans these directories recursively.
32+
33+
### `presets` (string[])
34+
A list of NPM packages to load as presets.
35+
ObjectQL will try to resolve the package and load schema files from its directory.
36+
Useful for sharing common business objects (User, Role, File, etc.).
37+
38+
### `plugins` (ObjectQLPlugin[])
39+
A list of plugin instances to extend the core functionality.
40+
See [Plugin System](./plugins.html) for details.
41+
42+
### `objects` (Record<string, ObjectConfig>)
43+
(Advanced) In-memory definition of objects. Useful for dynamic runtime schema generation.
44+
Objects defined here take highest priority.

docs/guide/getting-started.md

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,38 +39,36 @@ fields:
3939
default: false
4040
```
4141
42-
### 2. Initialize the Engine
42+
### 2. Configure the Engine
43+
44+
Updated in v0.2: You can now use a simple connection string.
4345
4446
```typescript
4547
import { ObjectQL } from '@objectql/core';
46-
import { KnexDriver } from '@objectql/driver-knex';
48+
import * as path from 'path';
4749

4850
async function main() {
49-
// 1. Configure the engine
50-
const app = new ObjectQL({
51-
datasources: {
52-
default: new KnexDriver({
53-
client: 'pg',
54-
connection: 'postgres://user:pass@localhost:5432/mydb'
55-
})
56-
},
57-
objects: {
58-
// In a real app, you can load these from a directory using app.loadFromDirectory()
59-
todo: {
60-
name: 'todo',
61-
fields: {
62-
title: { type: 'text' },
63-
completed: { type: 'boolean' }
64-
}
65-
}
66-
}
51+
const db = new ObjectQL({
52+
// 1. Connection String (Protocol://Path)
53+
// Detects 'sqlite', 'postgres', 'mongodb' automatically
54+
connection: 'sqlite://data.db',
55+
56+
// 2. Schema Source
57+
// Where your *.object.yml files are located
58+
source: ['src/objects'],
59+
60+
// 3. Load Presets (Optional)
61+
// Load standard objects from npm packages
62+
presets: ['@objectql/preset-auth']
6763
});
6864

69-
await app.init();
70-
71-
// 2. Create Data (CRUD)
65+
await db.init();
66+
67+
// ...
68+
69+
// 3. Create Data (CRUD)
7270
// Create a context (representing a user request)
73-
const ctx = app.createContext({});
71+
const ctx = db.createContext({});
7472
const todoRepo = ctx.object('todo');
7573

7674
const newTask = await todoRepo.create({
@@ -79,7 +77,7 @@ async function main() {
7977
});
8078
console.log('Created:', newTask);
8179

82-
// 3. Query Data
80+
// 4. Query Data
8381
const tasks = await todoRepo.find({
8482
filters: [['completed', '=', false]]
8583
});

docs/guide/plugins.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Plugin System
2+
3+
Plugins allow you to extend the core functionality of ObjectQL by intercepting lifecycle events, modifying metadata, or injecting new services.
4+
5+
## The Plugin Interface
6+
7+
A plugin is simply a class (or object) implementing `ObjectQLPlugin`.
8+
9+
```typescript
10+
import { IObjectQL } from '@objectql/types';
11+
12+
export interface ObjectQLPlugin {
13+
name: string;
14+
setup(app: IObjectQL): void | Promise<void>;
15+
}
16+
```
17+
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.
19+
20+
## Capabilities
21+
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()`.
25+
26+
## Example: Soft Delete Plugin
27+
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.
32+
33+
```typescript
34+
import { ObjectQLPlugin, IObjectQL } from '@objectql/types';
35+
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+
}
47+
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
58+
});
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+
});
70+
}
71+
}
72+
```
73+
74+
## Usage
75+
76+
```typescript
77+
const db = new ObjectQL({
78+
connection: 'sqlite://data.db',
79+
plugins: [
80+
new SoftDeletePlugin()
81+
]
82+
});
83+
```

examples/project-management/objectql.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import * as path from 'path';
33

44
const db = new ObjectQL({
55
connection: `sqlite://${path.join(__dirname, 'dev.sqlite3')}`,
6-
source: path.join(__dirname, 'src')
6+
source: ['src'],
7+
presets: []
78
});
89

910
export default db;

packages/core/src/index.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ObjectQLContextOptions,
77
IObjectQL,
88
ObjectQLConfig,
9+
ObjectQLPlugin,
910
HookName,
1011
HookHandler,
1112
HookContext,
@@ -22,6 +23,7 @@ export class ObjectQL implements IObjectQL {
2223
private datasources: Record<string, Driver> = {};
2324
private hooks: Record<string, Array<{ objectName: string, handler: HookHandler }>> = {};
2425
private actions: Record<string, ActionHandler> = {};
26+
private pluginsList: ObjectQLPlugin[] = [];
2527

2628
constructor(config: ObjectQLConfig) {
2729
this.metadata = config.registry || new MetadataRegistry();
@@ -32,26 +34,48 @@ export class ObjectQL implements IObjectQL {
3234
this.loadDriverFromConnection(config.connection);
3335
}
3436

37+
// 1. Load Presets/Packages first (Base Layer)
38+
if (config.packages) {
39+
for (const name of config.packages) {
40+
this.addPackage(name);
41+
}
42+
}
43+
if (config.presets) {
44+
for (const name of config.presets) {
45+
this.addPackage(name);
46+
}
47+
}
48+
49+
if (config.plugins) {
50+
for (const plugin of config.plugins) {
51+
this.use(plugin);
52+
}
53+
}
54+
55+
// 2. Load Local Sources (Application Layer - can override presets)
3556
if (config.source) {
36-
this.loader.load(config.source);
57+
const sources = Array.isArray(config.source) ? config.source : [config.source];
58+
for (const src of sources) {
59+
this.loader.load(src);
60+
}
3761
}
3862

63+
// 3. Load In-Memory Objects (Dynamic Layer - highest priority)
3964
if (config.objects) {
4065
for (const [key, obj] of Object.entries(config.objects)) {
4166
this.registerObject(obj);
4267
}
4368
}
44-
if (config.packages) {
45-
for (const name of config.packages) {
46-
this.addPackage(name);
47-
}
48-
}
4969
}
5070

5171
addPackage(name: string) {
5272
this.loader.loadPackage(name);
5373
}
5474

75+
use(plugin: ObjectQLPlugin) {
76+
this.pluginsList.push(plugin);
77+
}
78+
5579
removePackage(name: string) {
5680
this.metadata.unregisterPackage(name);
5781
}
@@ -172,6 +196,12 @@ export class ObjectQL implements IObjectQL {
172196
}
173197

174198
async init() {
199+
// 0. Init Plugins
200+
for (const plugin of this.pluginsList) {
201+
console.log(`Initializing plugin '${plugin.name}'...`);
202+
await plugin.setup(this);
203+
}
204+
175205
const objects = this.metadata.list<ObjectConfig>('object');
176206

177207
// 1. Init Drivers (e.g. Sync Schema)

packages/types/src/types.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export interface IObjectRepository {
2626
execute(actionName: string, id: string | number | undefined, params: any): Promise<any>;
2727
}
2828

29+
export interface ObjectQLPlugin {
30+
name: string;
31+
setup(app: IObjectQL): void | Promise<void>;
32+
}
33+
2934
export interface ObjectQLConfig {
3035
registry?: MetadataRegistry;
3136
datasources?: Record<string, Driver>;
@@ -35,11 +40,22 @@ export interface ObjectQLConfig {
3540
*/
3641
connection?: string;
3742
/**
38-
* Path to the directory containing schema files (*.object.yml).
43+
* Path(s) to the directory containing schema files (*.object.yml).
3944
*/
40-
source?: string;
45+
source?: string | string[];
4146
objects?: Record<string, ObjectConfig>;
47+
/**
48+
* @deprecated Use 'presets' instead.
49+
*/
4250
packages?: string[];
51+
/**
52+
* List of npm packages or presets to load.
53+
*/
54+
presets?: string[];
55+
/**
56+
* List of plugins to load.
57+
*/
58+
plugins?: ObjectQLPlugin[];
4359
}
4460

4561
export interface IObjectQL {

0 commit comments

Comments
 (0)