@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 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 ("linkable" in input) {
59
- return input;
58
+ if ("tableName" in input) {
59
+ return { linkable: input };
60
60
  }
61
- return { linkable: input };
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.assign(entity, data);
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 ("linkable" in input) {
9
- return input;
8
+ if ("tableName" in input) {
9
+ return { linkable: input };
10
10
  }
11
- return { linkable: input };
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.assign(entity, data);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridianjs/framework-utils",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Utilities for building Meridian modules: DML, service factory, defineModule, defineLink",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",