@meridianjs/framework-utils 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/dist/index.d.mts +0 -6
- package/dist/index.d.ts +0 -6
- package/dist/index.js +31 -10
- package/dist/index.mjs +31 -10
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# @meridianjs/framework-utils
|
|
2
|
+
|
|
3
|
+
Core building blocks for creating MeridianJS modules: the DML (Data Modelling Language), the `MeridianService` factory, `Module()`, `defineLink()`, and ORM utilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @meridianjs/framework-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Every domain module in a MeridianJS application is built with the primitives this package exports. You define your data models with `model`, generate CRUD services with `MeridianService`, wire up a module with `Module()`, and declare cross-module relationships with `defineLink()`.
|
|
14
|
+
|
|
15
|
+
## API Reference
|
|
16
|
+
|
|
17
|
+
### `model` — Data Modelling Language
|
|
18
|
+
|
|
19
|
+
Define a database entity without writing MikroORM decorators:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { model } from "@meridianjs/framework-utils"
|
|
23
|
+
|
|
24
|
+
export const Project = model("project", {
|
|
25
|
+
id: model.id(), // UUID primary key, auto-generated
|
|
26
|
+
name: model.text(), // VARCHAR
|
|
27
|
+
description: model.text(),
|
|
28
|
+
color: model.text(),
|
|
29
|
+
is_active: model.boolean(),
|
|
30
|
+
sort_order: model.number(),
|
|
31
|
+
metadata: model.json(),
|
|
32
|
+
status: model.enum(["active", "archived"]),
|
|
33
|
+
created_at: model.dateTime(),
|
|
34
|
+
updated_at: model.dateTime(),
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`model.id()` automatically creates a `uuid` primary key with `onCreate` auto-generation. `model.dateTime()` fields named `created_at` / `updated_at` are set automatically on create and update.
|
|
39
|
+
|
|
40
|
+
### `MeridianService` — Auto-generated CRUD
|
|
41
|
+
|
|
42
|
+
Pass a map of model names to model definitions and receive a class with full CRUD:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { MeridianService } from "@meridianjs/framework-utils"
|
|
46
|
+
import type { MeridianContainer } from "@meridianjs/types"
|
|
47
|
+
import { Project } from "./models/project.js"
|
|
48
|
+
import { Label } from "./models/label.js"
|
|
49
|
+
|
|
50
|
+
export class ProjectModuleService extends MeridianService({ Project, Label }) {
|
|
51
|
+
constructor(container: MeridianContainer) {
|
|
52
|
+
super(container)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Auto-generated for Project:
|
|
56
|
+
// listProjects(filters?, options?)
|
|
57
|
+
// listAndCountProjects(filters?, options?)
|
|
58
|
+
// retrieveProject(id)
|
|
59
|
+
// createProject(data)
|
|
60
|
+
// updateProject(id, data)
|
|
61
|
+
// deleteProject(id)
|
|
62
|
+
// softDeleteProject(id)
|
|
63
|
+
|
|
64
|
+
// Auto-generated for Label:
|
|
65
|
+
// listLabels, retrieveLabel, createLabel, updateLabel, deleteLabel ...
|
|
66
|
+
|
|
67
|
+
// Add custom methods here
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `Module()` — Module Definition
|
|
72
|
+
|
|
73
|
+
Register a service + its models and loaders with the DI container:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Module } from "@meridianjs/framework-utils"
|
|
77
|
+
|
|
78
|
+
export default Module("projectModuleService", {
|
|
79
|
+
service: ProjectModuleService,
|
|
80
|
+
models: [Project, Label, Milestone],
|
|
81
|
+
loaders: [defaultLoader],
|
|
82
|
+
linkable: {
|
|
83
|
+
project: { tableName: "project", primaryKey: "id" },
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `key` (first argument) is the container registration token — this is the name used in `container.resolve("projectModuleService")` and in route handlers via `req.scope.resolve("projectModuleService")`.
|
|
89
|
+
|
|
90
|
+
### `defineLink()` — Cross-module Relationships
|
|
91
|
+
|
|
92
|
+
Declare a join table between two modules without coupling them via foreign keys:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { defineLink } from "@meridianjs/framework-utils"
|
|
96
|
+
import ProjectModule from "@meridianjs/project"
|
|
97
|
+
import IssueModule from "@meridianjs/issue"
|
|
98
|
+
|
|
99
|
+
export default defineLink(
|
|
100
|
+
{ linkable: ProjectModule.linkable!.project },
|
|
101
|
+
{ linkable: IssueModule.linkable!.issue, isList: true, deleteCascades: true },
|
|
102
|
+
{ linkTableName: "project_issue_link", entryPoint: "projectIssueLink" }
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### ORM Utilities
|
|
107
|
+
|
|
108
|
+
Used inside module loaders to initialise per-module MikroORM instances:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { dmlToEntitySchema, createModuleOrm, createRepository } from "@meridianjs/framework-utils"
|
|
112
|
+
import type { LoaderOptions } from "@meridianjs/types"
|
|
113
|
+
import { Project } from "../models/project.js"
|
|
114
|
+
|
|
115
|
+
const ProjectSchema = dmlToEntitySchema(Project)
|
|
116
|
+
|
|
117
|
+
export default async function defaultLoader({ container }: LoaderOptions) {
|
|
118
|
+
const config = container.resolve("config") as any
|
|
119
|
+
const orm = await createModuleOrm([ProjectSchema], config.projectConfig.databaseUrl)
|
|
120
|
+
const em = orm.em.fork()
|
|
121
|
+
container.register({
|
|
122
|
+
projectRepository: createRepository(em, "project"),
|
|
123
|
+
projectOrm: orm,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Each module manages its own `MikroORM` instance and schema, keeping modules fully isolated from one another.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -156,12 +156,6 @@ type InferModel<M extends ModelDefinition> = M extends ModelDefinition<infer Sch
|
|
|
156
156
|
*/
|
|
157
157
|
declare function MeridianService(models: Record<string, ModelDefinition>): new (container: MeridianContainer) => IModuleService;
|
|
158
158
|
|
|
159
|
-
/**
|
|
160
|
-
* Converts a DML ModelDefinition to a MikroORM EntitySchema.
|
|
161
|
-
*
|
|
162
|
-
* Automatically adds `created_at`, `updated_at`, and `deleted_at` timestamp
|
|
163
|
-
* columns to every entity.
|
|
164
|
-
*/
|
|
165
159
|
declare function dmlToEntitySchema(def: ModelDefinition): EntitySchema;
|
|
166
160
|
/**
|
|
167
161
|
* Wraps a MikroORM EntityManager into the Repository interface expected
|
package/dist/index.d.ts
CHANGED
|
@@ -156,12 +156,6 @@ type InferModel<M extends ModelDefinition> = M extends ModelDefinition<infer Sch
|
|
|
156
156
|
*/
|
|
157
157
|
declare function MeridianService(models: Record<string, ModelDefinition>): new (container: MeridianContainer) => IModuleService;
|
|
158
158
|
|
|
159
|
-
/**
|
|
160
|
-
* Converts a DML ModelDefinition to a MikroORM EntitySchema.
|
|
161
|
-
*
|
|
162
|
-
* Automatically adds `created_at`, `updated_at`, and `deleted_at` timestamp
|
|
163
|
-
* columns to every entity.
|
|
164
|
-
*/
|
|
165
159
|
declare function dmlToEntitySchema(def: ModelDefinition): EntitySchema;
|
|
166
160
|
/**
|
|
167
161
|
* Wraps a MikroORM EntityManager into the Repository interface expected
|
package/dist/index.js
CHANGED
|
@@ -55,10 +55,10 @@ function Module(key, definition) {
|
|
|
55
55
|
|
|
56
56
|
// src/define-link.ts
|
|
57
57
|
function normalizeEndpoint(input) {
|
|
58
|
-
if ("
|
|
59
|
-
return input;
|
|
58
|
+
if ("tableName" in input) {
|
|
59
|
+
return { linkable: input };
|
|
60
60
|
}
|
|
61
|
-
return
|
|
61
|
+
return input;
|
|
62
62
|
}
|
|
63
63
|
function defineLink(left, right, options) {
|
|
64
64
|
const leftEndpoint = normalizeEndpoint(left);
|
|
@@ -184,6 +184,15 @@ var model = {
|
|
|
184
184
|
};
|
|
185
185
|
|
|
186
186
|
// src/service-factory.ts
|
|
187
|
+
var UPDATE_RESERVED = /* @__PURE__ */ new Set([
|
|
188
|
+
"id",
|
|
189
|
+
"created_at",
|
|
190
|
+
"updated_at",
|
|
191
|
+
"deleted_at",
|
|
192
|
+
"__proto__",
|
|
193
|
+
"constructor",
|
|
194
|
+
"prototype"
|
|
195
|
+
]);
|
|
187
196
|
function MeridianService(models) {
|
|
188
197
|
class BaseService {
|
|
189
198
|
// Use private class field to avoid conflicting with the index signature
|
|
@@ -200,21 +209,21 @@ function MeridianService(models) {
|
|
|
200
209
|
if (!hasCustomMethod(listMethod)) {
|
|
201
210
|
this[listMethod] = async (filters = {}, options = {}) => {
|
|
202
211
|
const repo = this.#container.resolve(repoToken);
|
|
203
|
-
return repo.find(filters, options);
|
|
212
|
+
return repo.find({ deleted_at: null, ...filters }, options);
|
|
204
213
|
};
|
|
205
214
|
}
|
|
206
215
|
const listAndCountMethod = `listAndCount${capitalizedPlural}`;
|
|
207
216
|
if (!hasCustomMethod(listAndCountMethod)) {
|
|
208
217
|
this[listAndCountMethod] = async (filters = {}, options = {}) => {
|
|
209
218
|
const repo = this.#container.resolve(repoToken);
|
|
210
|
-
return repo.findAndCount(filters, options);
|
|
219
|
+
return repo.findAndCount({ deleted_at: null, ...filters }, options);
|
|
211
220
|
};
|
|
212
221
|
}
|
|
213
222
|
const retrieveMethod = `retrieve${capitalized}`;
|
|
214
223
|
if (!hasCustomMethod(retrieveMethod)) {
|
|
215
224
|
this[retrieveMethod] = async (id) => {
|
|
216
225
|
const repo = this.#container.resolve(repoToken);
|
|
217
|
-
return repo.findOneOrFail({ id });
|
|
226
|
+
return repo.findOneOrFail({ id, deleted_at: null });
|
|
218
227
|
};
|
|
219
228
|
}
|
|
220
229
|
const createMethod = `create${capitalized}`;
|
|
@@ -230,8 +239,11 @@ function MeridianService(models) {
|
|
|
230
239
|
if (!hasCustomMethod(updateMethod)) {
|
|
231
240
|
this[updateMethod] = async (id, data) => {
|
|
232
241
|
const repo = this.#container.resolve(repoToken);
|
|
233
|
-
const entity = await repo.findOneOrFail({ id });
|
|
234
|
-
Object.
|
|
242
|
+
const entity = await repo.findOneOrFail({ id, deleted_at: null });
|
|
243
|
+
const safe = Object.fromEntries(
|
|
244
|
+
Object.entries(data).filter(([k]) => !UPDATE_RESERVED.has(k))
|
|
245
|
+
);
|
|
246
|
+
Object.assign(entity, safe);
|
|
235
247
|
await repo.flush();
|
|
236
248
|
return entity;
|
|
237
249
|
};
|
|
@@ -248,7 +260,7 @@ function MeridianService(models) {
|
|
|
248
260
|
if (!hasCustomMethod(softDeleteMethod)) {
|
|
249
261
|
this[softDeleteMethod] = async (id) => {
|
|
250
262
|
const repo = this.#container.resolve(repoToken);
|
|
251
|
-
const entity = await repo.findOneOrFail({ id });
|
|
263
|
+
const entity = await repo.findOneOrFail({ id, deleted_at: null });
|
|
252
264
|
entity.deleted_at = /* @__PURE__ */ new Date();
|
|
253
265
|
await repo.flush();
|
|
254
266
|
return entity;
|
|
@@ -262,7 +274,15 @@ function MeridianService(models) {
|
|
|
262
274
|
|
|
263
275
|
// src/orm-utils.ts
|
|
264
276
|
var import_core = require("@mikro-orm/core");
|
|
277
|
+
var RESERVED_TIMESTAMP_KEYS = ["created_at", "updated_at", "deleted_at"];
|
|
265
278
|
function dmlToEntitySchema(def) {
|
|
279
|
+
for (const key of RESERVED_TIMESTAMP_KEYS) {
|
|
280
|
+
if (key in def.schema) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Model "${def.tableName}" defines reserved column "${key}". Meridian automatically manages created_at, updated_at, and deleted_at.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
266
286
|
const properties = {};
|
|
267
287
|
for (const [key, prop] of Object.entries(def.schema)) {
|
|
268
288
|
if (prop instanceof IdProperty) {
|
|
@@ -344,9 +364,10 @@ function createRepository(em, entityName) {
|
|
|
344
364
|
return repo.find(filters, options);
|
|
345
365
|
},
|
|
346
366
|
async findAndCount(filters, options = {}) {
|
|
367
|
+
const { limit, offset, ...countOptions } = options;
|
|
347
368
|
const [data, count] = await Promise.all([
|
|
348
369
|
repo.find(filters, options),
|
|
349
|
-
repo.count(filters)
|
|
370
|
+
repo.count(filters, countOptions)
|
|
350
371
|
]);
|
|
351
372
|
return [data, count];
|
|
352
373
|
},
|
package/dist/index.mjs
CHANGED
|
@@ -5,10 +5,10 @@ function Module(key, definition) {
|
|
|
5
5
|
|
|
6
6
|
// src/define-link.ts
|
|
7
7
|
function normalizeEndpoint(input) {
|
|
8
|
-
if ("
|
|
9
|
-
return input;
|
|
8
|
+
if ("tableName" in input) {
|
|
9
|
+
return { linkable: input };
|
|
10
10
|
}
|
|
11
|
-
return
|
|
11
|
+
return input;
|
|
12
12
|
}
|
|
13
13
|
function defineLink(left, right, options) {
|
|
14
14
|
const leftEndpoint = normalizeEndpoint(left);
|
|
@@ -134,6 +134,15 @@ var model = {
|
|
|
134
134
|
};
|
|
135
135
|
|
|
136
136
|
// src/service-factory.ts
|
|
137
|
+
var UPDATE_RESERVED = /* @__PURE__ */ new Set([
|
|
138
|
+
"id",
|
|
139
|
+
"created_at",
|
|
140
|
+
"updated_at",
|
|
141
|
+
"deleted_at",
|
|
142
|
+
"__proto__",
|
|
143
|
+
"constructor",
|
|
144
|
+
"prototype"
|
|
145
|
+
]);
|
|
137
146
|
function MeridianService(models) {
|
|
138
147
|
class BaseService {
|
|
139
148
|
// Use private class field to avoid conflicting with the index signature
|
|
@@ -150,21 +159,21 @@ function MeridianService(models) {
|
|
|
150
159
|
if (!hasCustomMethod(listMethod)) {
|
|
151
160
|
this[listMethod] = async (filters = {}, options = {}) => {
|
|
152
161
|
const repo = this.#container.resolve(repoToken);
|
|
153
|
-
return repo.find(filters, options);
|
|
162
|
+
return repo.find({ deleted_at: null, ...filters }, options);
|
|
154
163
|
};
|
|
155
164
|
}
|
|
156
165
|
const listAndCountMethod = `listAndCount${capitalizedPlural}`;
|
|
157
166
|
if (!hasCustomMethod(listAndCountMethod)) {
|
|
158
167
|
this[listAndCountMethod] = async (filters = {}, options = {}) => {
|
|
159
168
|
const repo = this.#container.resolve(repoToken);
|
|
160
|
-
return repo.findAndCount(filters, options);
|
|
169
|
+
return repo.findAndCount({ deleted_at: null, ...filters }, options);
|
|
161
170
|
};
|
|
162
171
|
}
|
|
163
172
|
const retrieveMethod = `retrieve${capitalized}`;
|
|
164
173
|
if (!hasCustomMethod(retrieveMethod)) {
|
|
165
174
|
this[retrieveMethod] = async (id) => {
|
|
166
175
|
const repo = this.#container.resolve(repoToken);
|
|
167
|
-
return repo.findOneOrFail({ id });
|
|
176
|
+
return repo.findOneOrFail({ id, deleted_at: null });
|
|
168
177
|
};
|
|
169
178
|
}
|
|
170
179
|
const createMethod = `create${capitalized}`;
|
|
@@ -180,8 +189,11 @@ function MeridianService(models) {
|
|
|
180
189
|
if (!hasCustomMethod(updateMethod)) {
|
|
181
190
|
this[updateMethod] = async (id, data) => {
|
|
182
191
|
const repo = this.#container.resolve(repoToken);
|
|
183
|
-
const entity = await repo.findOneOrFail({ id });
|
|
184
|
-
Object.
|
|
192
|
+
const entity = await repo.findOneOrFail({ id, deleted_at: null });
|
|
193
|
+
const safe = Object.fromEntries(
|
|
194
|
+
Object.entries(data).filter(([k]) => !UPDATE_RESERVED.has(k))
|
|
195
|
+
);
|
|
196
|
+
Object.assign(entity, safe);
|
|
185
197
|
await repo.flush();
|
|
186
198
|
return entity;
|
|
187
199
|
};
|
|
@@ -198,7 +210,7 @@ function MeridianService(models) {
|
|
|
198
210
|
if (!hasCustomMethod(softDeleteMethod)) {
|
|
199
211
|
this[softDeleteMethod] = async (id) => {
|
|
200
212
|
const repo = this.#container.resolve(repoToken);
|
|
201
|
-
const entity = await repo.findOneOrFail({ id });
|
|
213
|
+
const entity = await repo.findOneOrFail({ id, deleted_at: null });
|
|
202
214
|
entity.deleted_at = /* @__PURE__ */ new Date();
|
|
203
215
|
await repo.flush();
|
|
204
216
|
return entity;
|
|
@@ -212,7 +224,15 @@ function MeridianService(models) {
|
|
|
212
224
|
|
|
213
225
|
// src/orm-utils.ts
|
|
214
226
|
import { EntitySchema } from "@mikro-orm/core";
|
|
227
|
+
var RESERVED_TIMESTAMP_KEYS = ["created_at", "updated_at", "deleted_at"];
|
|
215
228
|
function dmlToEntitySchema(def) {
|
|
229
|
+
for (const key of RESERVED_TIMESTAMP_KEYS) {
|
|
230
|
+
if (key in def.schema) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Model "${def.tableName}" defines reserved column "${key}". Meridian automatically manages created_at, updated_at, and deleted_at.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
216
236
|
const properties = {};
|
|
217
237
|
for (const [key, prop] of Object.entries(def.schema)) {
|
|
218
238
|
if (prop instanceof IdProperty) {
|
|
@@ -294,9 +314,10 @@ function createRepository(em, entityName) {
|
|
|
294
314
|
return repo.find(filters, options);
|
|
295
315
|
},
|
|
296
316
|
async findAndCount(filters, options = {}) {
|
|
317
|
+
const { limit, offset, ...countOptions } = options;
|
|
297
318
|
const [data, count] = await Promise.all([
|
|
298
319
|
repo.find(filters, options),
|
|
299
|
-
repo.count(filters)
|
|
320
|
+
repo.count(filters, countOptions)
|
|
300
321
|
]);
|
|
301
322
|
return [data, count];
|
|
302
323
|
},
|
package/package.json
CHANGED