@atscript/moost-mongo 0.0.18

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Atscript
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # @atscript/moost-mongo
2
+
3
+ Simple generator‑free CRUD for **MongoDB** collections defined with
4
+ **atscript** and served by
5
+ [Moost](https://moost.org/).
6
+
7
+ - ✅ **Zero boilerplate** – one decorator turns your `.as` model into a fully‑featured controller.
8
+ - 🔌 **Pluggable** – override protected hooks to adjust validation, projections, or write logic.
9
+ - ⚙️ **URLQL** powered filtering / paging / projections on **GET /query** & **GET /pages**.
10
+ - 🧱 **Type‑safe** – everything is inferred from your annotated interface.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @atscript/moost-mongo # this package
18
+ pnpm add @atscript/mongo mongodb # runtime peer deps
19
+ pnpm add moost @moostjs/event-http # your HTTP adapter
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ### 1  Describe your collection
27
+
28
+ ```ts
29
+ // src/collections/user.collection.as
30
+ @mongo.collection 'users'
31
+ export interface User {
32
+ @mongo.index.unique
33
+ email: string
34
+ name: string
35
+ age: number
36
+ }
37
+ ```
38
+
39
+ ### 2  Subclass the controller
40
+
41
+ ```ts
42
+ import { AsMongoController, CollectionController } from '@atscript/moost-mongo'
43
+ import type { User } from '../collections/user.collection.as'
44
+
45
+ /* Provide AsMongo connection for the controller */
46
+ @Provide(AsMongo, () => new AsMongo(process.env.MONGO_URI!))
47
+ @CollectionController(User) // optional prefix param available
48
+ export class UsersController extends AsMongoController<typeof User> {}
49
+ ```
50
+
51
+ ### 3  Bootstrap Moost
52
+
53
+ ```ts
54
+ import { Moost } from 'moost'
55
+ import { MoostHttp } from '@moostjs/event-http'
56
+ import { UsersController } from './controllers/users.controller'
57
+
58
+ const app = new Moost()
59
+
60
+ void app.adapter(new MoostHttp()).listen(3000)
61
+ void app.registerControllers(UsersController).init()
62
+ ```
63
+
64
+ Hit the endpoints:
65
+
66
+ ```
67
+ GET /users/query ?$filter=age>18&$select=name,email
68
+ GET /users/pages ?$page=2&$size=20&$sort=age:-1
69
+ GET /users/one/{id}
70
+ POST /users (JSON body) – insert 1‒n documents
71
+ PUT /users (JSON body with _id) – replace
72
+ PATCH/DELETE analogously
73
+ ```
74
+
75
+ ---
76
+
77
+ ## API
78
+
79
+ ### `class AsMongoController<T>`
80
+
81
+ Base class you extend. **T** is the atscript constructor exported from the
82
+ `.as` file.
83
+
84
+ | Hook / Method | When to override | Typical use‑case |
85
+ | --------------------------- | ------------------------------------ | --------------------------------------- |
86
+ | `protected init()` | Once at controller creation | Create indexes, seed data |
87
+ | `transformProjection()` | Before running `find()` | Force whitelist / blacklist projections |
88
+ | `validate*Controls()` | Per endpoint | Custom URLQL control validation |
89
+ | `onRemove(id, opts)` | Before `deleteOne` | Soft‑delete or veto |
90
+ | `onWrite(action,data,opts)` | Before any insert / update / replace | Auto‑populate fields, audit, veto |
91
+
92
+ All CRUD endpoints are already wired – just subclass and go.
93
+
94
+ ### `CollectionController(type, prefix?)`
95
+
96
+ Decorator that glues your subclass to Moost:
97
+
98
+ 1. Registers the collection constructor under DI token `__atscript_mongo_collection_def`.
99
+ 2. Marks the class as a `@Controller(prefix)` (defaults to the collection name).
100
+ 3. Ensures route metadata from the parent is inherited (`@Inherit`).
101
+
102
+ ```ts
103
+ @CollectionController(User, 'users')
104
+ export class UsersController extends AsMongoController<typeof User> {}
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Route reference
110
+
111
+ | Route | Description | Query controls |
112
+ | ------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------- |
113
+ | `GET /<prefix>/query` | List documents / count mode | `$filter`, `$select`, `$sort`, `$limit`, `$skip`, `$count` |
114
+ | `GET /<prefix>/pages` | Paged list with meta | same + `$page`, `$size` |
115
+ | `GET /<prefix>/one/:id` | Single document by `_id` or any `@mongo.index.unique` field | `$select` only |
116
+ | `POST /<prefix>` | Insert one or many | – |
117
+ | `PUT /<prefix>` | Replace by `_id` | – |
118
+ | `PATCH /<prefix>` | Update by `_id` | – |
119
+ | `DELETE /<prefix>/:id` | Remove by `_id` | – |
120
+
121
+ ---
122
+
123
+ ## Extending example
124
+
125
+ ```ts
126
+ @CollectionController(User)
127
+ export class UsersController extends AsMongoController<typeof User> {
128
+ /** Add `createdAt` on every insert. */
129
+ protected async onWrite(action, data) {
130
+ if (action === 'insert' && data) {
131
+ ;(data as any).createdAt = new Date()
132
+ }
133
+ return data
134
+ }
135
+
136
+ /** Soft delete instead of hard delete. */
137
+ protected async onRemove(id, opts) {
138
+ await this.asCollection.collection.updateOne(
139
+ { _id: this.asCollection.prepareId(id) },
140
+ { $set: { deleted: true, deletedAt: new Date() } }
141
+ )
142
+ // prevent actual deleteOne
143
+ return undefined
144
+ }
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## License
151
+
152
+ MIT © 2025 Artem Maltsev
package/dist/index.cjs ADDED
@@ -0,0 +1,465 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const __moostjs_event_http = __toESM(require("@moostjs/event-http"));
26
+ const moost = __toESM(require("moost"));
27
+ const urlql = __toESM(require("urlql"));
28
+ const __atscript_typescript = __toESM(require("@atscript/typescript"));
29
+ const __atscript_mongo = __toESM(require("@atscript/mongo"));
30
+
31
+ //#region packages/moost-mongo/src/dto/controls.dto.as.js
32
+ function _define_property$1(obj, key, value) {
33
+ if (key in obj) Object.defineProperty(obj, key, {
34
+ value,
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true
38
+ });
39
+ else obj[key] = value;
40
+ return obj;
41
+ }
42
+ var QueryControlsDto = class {};
43
+ _define_property$1(QueryControlsDto, "__is_atscript_annotated_type", true);
44
+ _define_property$1(QueryControlsDto, "type", {});
45
+ _define_property$1(QueryControlsDto, "metadata", new Map());
46
+ var PagesControlsDto = class {};
47
+ _define_property$1(PagesControlsDto, "__is_atscript_annotated_type", true);
48
+ _define_property$1(PagesControlsDto, "type", {});
49
+ _define_property$1(PagesControlsDto, "metadata", new Map());
50
+ var GetOneControlsDto = class {};
51
+ _define_property$1(GetOneControlsDto, "__is_atscript_annotated_type", true);
52
+ _define_property$1(GetOneControlsDto, "type", {});
53
+ _define_property$1(GetOneControlsDto, "metadata", new Map());
54
+ let SortControlDto = class SortControlDto$1 {};
55
+ _define_property$1(SortControlDto, "__is_atscript_annotated_type", true);
56
+ _define_property$1(SortControlDto, "type", {});
57
+ _define_property$1(SortControlDto, "metadata", new Map());
58
+ let SelectControlDto = class SelectControlDto$1 {};
59
+ _define_property$1(SelectControlDto, "__is_atscript_annotated_type", true);
60
+ _define_property$1(SelectControlDto, "type", {});
61
+ _define_property$1(SelectControlDto, "metadata", new Map());
62
+ (0, __atscript_typescript.defineAnnotatedType)("object", QueryControlsDto).prop("$skip", (0, __atscript_typescript.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.min", 0).annotate("expect.int", true).optional().$type).prop("$limit", (0, __atscript_typescript.defineAnnotatedType)().designType("number").tags("positive", "int", "number").annotate("expect.min", 0).annotate("expect.int", true).optional().$type).prop("$count", (0, __atscript_typescript.defineAnnotatedType)().designType("boolean").tags("boolean").optional().$type).prop("$sort", (0, __atscript_typescript.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, __atscript_typescript.defineAnnotatedType)().refTo(SelectControlDto).optional().$type);
63
+ (0, __atscript_typescript.defineAnnotatedType)("object", PagesControlsDto).prop("$page", (0, __atscript_typescript.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
64
+ pattern: "^\\d+$",
65
+ flags: "u",
66
+ message: "Expected positive number"
67
+ }, true).optional().$type).prop("$size", (0, __atscript_typescript.defineAnnotatedType)().designType("string").tags("string").annotate("expect.pattern", {
68
+ pattern: "^\\d+$",
69
+ flags: "u",
70
+ message: "Expected positive number"
71
+ }, true).optional().$type).prop("$sort", (0, __atscript_typescript.defineAnnotatedType)().refTo(SortControlDto).optional().$type).prop("$select", (0, __atscript_typescript.defineAnnotatedType)().refTo(SelectControlDto).optional().$type);
72
+ (0, __atscript_typescript.defineAnnotatedType)("object", GetOneControlsDto).prop("$select", (0, __atscript_typescript.defineAnnotatedType)().refTo(SelectControlDto).optional().$type);
73
+ (0, __atscript_typescript.defineAnnotatedType)("object", SortControlDto).propPattern(/./, (0, __atscript_typescript.defineAnnotatedType)("union").item((0, __atscript_typescript.defineAnnotatedType)().designType("number").value(1).$type).item((0, __atscript_typescript.defineAnnotatedType)().designType("number").value(-1).$type).$type);
74
+ (0, __atscript_typescript.defineAnnotatedType)("object", SelectControlDto).propPattern(/./, (0, __atscript_typescript.defineAnnotatedType)("union").item((0, __atscript_typescript.defineAnnotatedType)().designType("number").value(1).$type).item((0, __atscript_typescript.defineAnnotatedType)().designType("number").value(0).$type).$type);
75
+
76
+ //#endregion
77
+ //#region packages/moost-mongo/src/decorators.ts
78
+ const COLLECTION_DEF = "__atscript_mongo_collection_def";
79
+ const CollectionController = (type, prefix) => {
80
+ return (0, moost.ApplyDecorators)((0, moost.Provide)(COLLECTION_DEF, () => type), (0, moost.Controller)(prefix || type.metadata.get("mongo.collection") || type.name), (0, moost.Inherit)());
81
+ };
82
+
83
+ //#endregion
84
+ //#region packages/moost-mongo/src/as-mongo.controller.ts
85
+ function _define_property(obj, key, value) {
86
+ if (key in obj) Object.defineProperty(obj, key, {
87
+ value,
88
+ enumerable: true,
89
+ configurable: true,
90
+ writable: true
91
+ });
92
+ else obj[key] = value;
93
+ return obj;
94
+ }
95
+ function _ts_decorate(decorators, target, key, desc) {
96
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
97
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
98
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
99
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
100
+ }
101
+ function _ts_metadata(k, v) {
102
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
103
+ }
104
+ function _ts_param(paramIndex, decorator) {
105
+ return function(target, key) {
106
+ decorator(target, key, paramIndex);
107
+ };
108
+ }
109
+ var AsMongoController = class {
110
+ /**
111
+ * One‑time initialization hook executed right after the collection is obtained.
112
+ *
113
+ * Default behaviour:
114
+ * * Automatically synchronises MongoDB indexes unless
115
+ * `mongo.autoIndexes` metadata flag is set to `false` on the model.
116
+ *
117
+ * Override to seed data, register change streams, etc. Both sync and async
118
+ * return types are supported.
119
+ */ init() {
120
+ if (this.type.metadata.get("mongo.autoIndexes") === false) {} else return this.asCollection.syncIndexes();
121
+ }
122
+ /** Returns (and memoises) validator for *query* endpoint controls. */ get queryControlsValidator() {
123
+ if (!this._queryControlsValidator) this._queryControlsValidator = QueryControlsDto.validator();
124
+ return this._queryControlsValidator;
125
+ }
126
+ /** Returns (and memoises) validator for *pages* endpoint controls. */ get pagesControlsValidator() {
127
+ if (!this._pagesControlsValidator) this._pagesControlsValidator = PagesControlsDto.validator();
128
+ return this._pagesControlsValidator;
129
+ }
130
+ /** Returns (and memoises) validator for *one* endpoint controls. */ get getOneControlsValidator() {
131
+ if (!this._getOneControlsValidator) this._getOneControlsValidator = GetOneControlsDto.validator();
132
+ return this._getOneControlsValidator;
133
+ }
134
+ /**
135
+ * Validates `$limit`, `$skip`, `$sort`, `$select`, `$count` controls for the
136
+ * **query** endpoint.
137
+ *
138
+ * @param controls - Controls object emitted by `urlql` parser.
139
+ * @returns Error message string or `undefined` (when valid). Can be async.
140
+ */ validateQueryControls(controls) {
141
+ this.queryControlsValidator.validate(controls);
142
+ return undefined;
143
+ }
144
+ /**
145
+ * Validates pagination‑specific controls for the **pages** endpoint.
146
+ *
147
+ * @param controls - Controls object emitted by `urlql` parser.
148
+ * @returns Error message string or `undefined` (when valid). Can be async.
149
+ */ validatePagesControls(controls) {
150
+ this.pagesControlsValidator.validate(controls);
151
+ return undefined;
152
+ }
153
+ /**
154
+ * Validates controls for the **one /: id ** endpoint.
155
+ *
156
+ * @param controls - Controls object emitted by `urlql` parser.
157
+ * @returns Error message string or `undefined` (when valid). Can be async.
158
+ */ validateGetOneControls(controls) {
159
+ this.getOneControlsValidator.validate(controls);
160
+ return undefined;
161
+ }
162
+ /**
163
+ * Validates the `insights` section ensuring only known projection fields are used.
164
+ *
165
+ * @param insights - Map of insight keys.
166
+ * @returns Error message string or `undefined` (when valid). Can be async.
167
+ */ validateInsights(insights) {
168
+ for (const key of insights.keys()) if (!this.asCollection.flatMap.has(key)) return `Unknown field "${key}"`;
169
+ return undefined;
170
+ }
171
+ /**
172
+ * Runs all validations relevant to current endpoint and returns a ready‑to‑send
173
+ * `HttpError` instance if something is invalid.
174
+ *
175
+ * @param parsed - Full parsed URLQL query.
176
+ * @param controlsType - Which controls validator to apply.
177
+ */ async validateUrlql(parsed, controlsType) {
178
+ const controlsValidators = {
179
+ query: this.validateQueryControls.bind(this),
180
+ pages: this.validatePagesControls.bind(this),
181
+ getOne: this.validateGetOneControls.bind(this)
182
+ };
183
+ try {
184
+ const error = await controlsValidators[controlsType](parsed.controls);
185
+ if (error) return new __moostjs_event_http.HttpError(400, error);
186
+ } catch (e) {
187
+ return new __moostjs_event_http.HttpError(400, e.message);
188
+ }
189
+ try {
190
+ const error = await this.validateInsights(parsed.insights);
191
+ if (error) return new __moostjs_event_http.HttpError(400, error);
192
+ } catch (e) {
193
+ return new __moostjs_event_http.HttpError(400, e.message);
194
+ }
195
+ }
196
+ /**
197
+ * Allows subclasses to translate field projection (e.g. whitelist vs blacklist).
198
+ *
199
+ * @param projection - Mongo projection generated by `urlql` `$select` control.
200
+ * @returns Adjusted projection (may return `Promise`).
201
+ */ transformProjection(projection) {
202
+ return projection;
203
+ }
204
+ /**
205
+ * Builds MongoDB `FindOptions` object out of URLQL controls.
206
+ *
207
+ * @param controls - Parsed `controls` object.
208
+ */ prepareQueryOptions(controls) {
209
+ return {
210
+ projection: this.transformProjection(controls.$select),
211
+ sort: controls.$sort,
212
+ limit: controls.$limit,
213
+ skip: controls.$skip
214
+ };
215
+ }
216
+ /**
217
+ * **GET /query** – returns an array of documents or a count depending on
218
+ * presence of `$count` control.
219
+ *
220
+ * @param url - Full request URL provided by Moost (includes query string).
221
+ * @returns Documents array **or** document count number.
222
+ */ async query(url) {
223
+ const query = url.split("?").slice(1).join("?");
224
+ const parsed = (0, urlql.parseUrlql)(query);
225
+ const error = await this.validateUrlql(parsed, "query");
226
+ if (error) return error;
227
+ return parsed.controls.$count ? this.asCollection.collection.countDocuments(parsed.filter) : this.asCollection.collection.find(parsed.filter, this.prepareQueryOptions(parsed.controls)).toArray();
228
+ }
229
+ /**
230
+ * **GET /pages** – returns paginated documents plus basic pagination meta.
231
+ *
232
+ * @param url - Full request URL.
233
+ * @returns An object with keys: `documents`, `page`, `size`, `totalPages`, `totalDocuments`.
234
+ */ async pages(url) {
235
+ const query = url.split("?").slice(1).join("?");
236
+ const parsed = (0, urlql.parseUrlql)(query);
237
+ const error = await this.validateUrlql(parsed, "pages");
238
+ if (error) return error;
239
+ const controls = parsed.controls;
240
+ const page = Math.max(Number(controls.$page || 1), 1);
241
+ const size = Math.max(Number(controls.$size || 10), 1);
242
+ const skip = (page - 1) * size;
243
+ const result = await this.asCollection.collection.aggregate([{ $match: parsed.filter }, { $facet: {
244
+ documents: [
245
+ controls.$sort ? { $sort: controls.$sort } : undefined,
246
+ { $skip: skip },
247
+ { $limit: size },
248
+ controls.$select ? { $project: controls.$select } : undefined
249
+ ].filter(Boolean),
250
+ meta: [{ $count: "count" }]
251
+ } }]).toArray();
252
+ const totalDocuments = result[0].meta[0].count;
253
+ return {
254
+ documents: result[0].documents,
255
+ page,
256
+ size,
257
+ totalPages: Math.ceil(totalDocuments / size),
258
+ totalDocuments
259
+ };
260
+ }
261
+ /**
262
+ * **GET /one/:id** – retrieves a single document. The identifier may be a
263
+ * Mongo `ObjectId` **or** the value of any `unique` property registered on
264
+ * the model.
265
+ *
266
+ * Filtering is not allowed on this route.
267
+ *
268
+ * @param id - Document `_id` or alternate unique key value.
269
+ * @param url - Full request URL (supports `$select`, `$insights`, etc.).
270
+ */ async getOne(id, url) {
271
+ const idValidator = this.asCollection.flatMap.get("_id")?.validator();
272
+ const query = url.split("?").slice(1).join("?");
273
+ const parsed = (0, urlql.parseUrlql)(query);
274
+ if (Object.keys(parsed.filter).length) return new __moostjs_event_http.HttpError(400, "Filtering is not allowed for \"one\" endpoint");
275
+ const error = await this.validateUrlql(parsed, "getOne");
276
+ if (error) return error;
277
+ if (idValidator?.validate(id, true)) return this.returnOne(this.asCollection.collection.find({ _id: this.asCollection.prepareId(id) }, this.prepareQueryOptions(parsed.controls)).toArray());
278
+ else if (this.asCollection.uniqueProps.size > 0) {
279
+ const filter = [];
280
+ for (const prop of this.asCollection.uniqueProps) filter.push({ [prop]: id });
281
+ return this.returnOne(this.asCollection.collection.find({ $or: filter }, this.prepareQueryOptions(parsed.controls)).toArray());
282
+ }
283
+ if (idValidator) return new __atscript_typescript.ValidatorError(idValidator.errors);
284
+ return new __moostjs_event_http.HttpError(500, "Unknown error");
285
+ }
286
+ /**
287
+ * Helper that unwraps a promise returning an array of documents and
288
+ * guarantees zero‑or‑one semantics expected by **one** endpoint.
289
+ *
290
+ * @param result - Promise resolving to an array of documents.
291
+ * @returns Document, 400 or 404 { @link HttpError }.
292
+ */ async returnOne(result) {
293
+ const items = await result;
294
+ if (items.length > 1) return new __moostjs_event_http.HttpError(400, "Found more than one record");
295
+ else if (items.length === 0) return new __moostjs_event_http.HttpError(404);
296
+ else return items[0];
297
+ }
298
+ /**
299
+ * **POST /** – inserts one or many documents.
300
+ *
301
+ * @param payload - Raw request body to be inserted.
302
+ */ async insert(payload) {
303
+ const data = this.asCollection.prepareInsert(payload);
304
+ const opts = {};
305
+ if (Array.isArray(data)) {
306
+ const newData = await this.onWrite("insertMany", data, opts);
307
+ if (newData) return this.asCollection.collection.insertMany(newData, opts);
308
+ else return new __moostjs_event_http.HttpError(500, "Not saved");
309
+ }
310
+ if (data) {
311
+ const newData = await this.onWrite("insert", data, opts);
312
+ if (newData) return this.asCollection.collection.insertOne(newData, opts);
313
+ else return new __moostjs_event_http.HttpError(500, "Not saved");
314
+ }
315
+ return new __moostjs_event_http.HttpError(500, "Not saved");
316
+ }
317
+ /**
318
+ * **PUT /** – fully replaces a document matched by `_id`.
319
+ *
320
+ * @param payload - Object containing `_id` plus full replacement document.
321
+ */ async replace(payload) {
322
+ const args = this.asCollection.prepareReplace(payload).toArgs();
323
+ const newData = await this.onWrite("replace", args[1], args[2]);
324
+ if (newData) return this.asCollection.collection.replaceOne(args[0], newData, args[2]);
325
+ return new __moostjs_event_http.HttpError(500, "Not saved");
326
+ }
327
+ /**
328
+ * **PATCH /** – updates one document using MongoDB update operators.
329
+ *
330
+ * @param payload - Update payload produced by `asCollection.prepareUpdate`.
331
+ */ async update(payload) {
332
+ const args = this.asCollection.prepareUpdate(payload).toArgs();
333
+ const newData = await this.onWrite("update", args[1], args[2]);
334
+ if (newData) return this.asCollection.collection.updateOne(args[0], newData, args[2]);
335
+ return new __moostjs_event_http.HttpError(500, "Not saved");
336
+ }
337
+ /**
338
+ * **DELETE /:id** – removes a single document by `_id`.
339
+ *
340
+ * @param id - Document identifier.
341
+ */ async remove(id) {
342
+ const opts = {};
343
+ id = await this.onRemove(id, opts);
344
+ if (id !== undefined) {
345
+ const result = await this.asCollection.collection.deleteOne({ _id: this.asCollection.prepareId(id) }, opts);
346
+ if (result.deletedCount < 1) throw new __moostjs_event_http.HttpError(404);
347
+ return result;
348
+ }
349
+ return new __moostjs_event_http.HttpError(500, "Not deleted");
350
+ }
351
+ /**
352
+ * Intercepts delete operation allowing subclasses to veto or mutate it.
353
+ *
354
+ * @param id - Requested document ID.
355
+ * @param opts - Mutable `DeleteOptions` passed to Mongo driver.
356
+ * @returns Final ID to be deleted (can be async).
357
+ */ onRemove(id, opts) {
358
+ return id;
359
+ }
360
+ onWrite(action, data, opts) {
361
+ return data;
362
+ }
363
+ /**
364
+ * Creates a controller instance and resolves the underlying collection.
365
+ *
366
+ * > Do **not** perform heavy asynchronous work directly inside the
367
+ * > constructor – override {@link init} instead.
368
+ *
369
+ * @param asMongo - Shared `AsMongo` driver instance.
370
+ * @param type - AtScript annotated model constructor for the collection.
371
+ * @param app - The current `Moost` application (used for retrieving a logger).
372
+ * @throws Rethrows any error emitted from {@link init} to stop controller registration.
373
+ */ constructor(asMongo, type, app) {
374
+ _define_property(this, "asMongo", void 0);
375
+ _define_property(this, "type", void 0);
376
+ /** Reference to the lazily created {@link AsCollection}. */ _define_property(this, "asCollection", void 0);
377
+ /** Application‑scoped logger bound to the collection name. */ _define_property(this, "logger", void 0);
378
+ _define_property(this, "_queryControlsValidator", void 0);
379
+ _define_property(this, "_pagesControlsValidator", void 0);
380
+ _define_property(this, "_getOneControlsValidator", void 0);
381
+ this.asMongo = asMongo;
382
+ this.type = type;
383
+ this.logger = app.getLogger(`mongo [${type.metadata.get("mongo.collection") || ""}]`);
384
+ this.asCollection = this.asMongo.getCollection(type, this.logger);
385
+ this.logger.info(`Initializing Collection`);
386
+ try {
387
+ const p = this.init();
388
+ if (p instanceof Promise) p.catch((e) => {
389
+ this.logger.error(e);
390
+ });
391
+ } catch (e) {
392
+ this.logger.error(e);
393
+ throw e;
394
+ }
395
+ }
396
+ };
397
+ _ts_decorate([
398
+ (0, __moostjs_event_http.Get)("query"),
399
+ _ts_param(0, (0, __moostjs_event_http.Url)()),
400
+ _ts_metadata("design:type", Function),
401
+ _ts_metadata("design:paramtypes", [String]),
402
+ _ts_metadata("design:returntype", Promise)
403
+ ], AsMongoController.prototype, "query", null);
404
+ _ts_decorate([
405
+ (0, __moostjs_event_http.Get)("pages"),
406
+ _ts_param(0, (0, __moostjs_event_http.Url)()),
407
+ _ts_metadata("design:type", Function),
408
+ _ts_metadata("design:paramtypes", [String]),
409
+ _ts_metadata("design:returntype", Promise)
410
+ ], AsMongoController.prototype, "pages", null);
411
+ _ts_decorate([
412
+ (0, __moostjs_event_http.Get)("one/:id"),
413
+ _ts_param(0, (0, moost.Param)("id")),
414
+ _ts_param(1, (0, __moostjs_event_http.Url)()),
415
+ _ts_metadata("design:type", Function),
416
+ _ts_metadata("design:paramtypes", [String, String]),
417
+ _ts_metadata("design:returntype", Promise)
418
+ ], AsMongoController.prototype, "getOne", null);
419
+ _ts_decorate([
420
+ (0, __moostjs_event_http.Post)(""),
421
+ _ts_param(0, (0, __moostjs_event_http.Body)()),
422
+ _ts_metadata("design:type", Function),
423
+ _ts_metadata("design:paramtypes", [Object]),
424
+ _ts_metadata("design:returntype", Promise)
425
+ ], AsMongoController.prototype, "insert", null);
426
+ _ts_decorate([
427
+ (0, __moostjs_event_http.Put)(""),
428
+ _ts_param(0, (0, __moostjs_event_http.Body)()),
429
+ _ts_metadata("design:type", Function),
430
+ _ts_metadata("design:paramtypes", [Object]),
431
+ _ts_metadata("design:returntype", Promise)
432
+ ], AsMongoController.prototype, "replace", null);
433
+ _ts_decorate([
434
+ (0, __moostjs_event_http.Patch)(""),
435
+ _ts_param(0, (0, __moostjs_event_http.Body)()),
436
+ _ts_metadata("design:type", Function),
437
+ _ts_metadata("design:paramtypes", [Object]),
438
+ _ts_metadata("design:returntype", Promise)
439
+ ], AsMongoController.prototype, "update", null);
440
+ _ts_decorate([
441
+ (0, __moostjs_event_http.Delete)(":id"),
442
+ _ts_param(0, (0, moost.Param)("id")),
443
+ _ts_metadata("design:type", Function),
444
+ _ts_metadata("design:paramtypes", [String]),
445
+ _ts_metadata("design:returntype", Promise)
446
+ ], AsMongoController.prototype, "remove", null);
447
+ AsMongoController = _ts_decorate([
448
+ _ts_param(1, (0, moost.Inject)(COLLECTION_DEF)),
449
+ _ts_metadata("design:type", Function),
450
+ _ts_metadata("design:paramtypes", [
451
+ typeof __atscript_mongo.AsMongo === "undefined" ? Object : __atscript_mongo.AsMongo,
452
+ typeof T === "undefined" ? Object : T,
453
+ typeof moost.Moost === "undefined" ? Object : moost.Moost
454
+ ])
455
+ ], AsMongoController);
456
+
457
+ //#endregion
458
+ Object.defineProperty(exports, 'AsMongoController', {
459
+ enumerable: true,
460
+ get: function () {
461
+ return AsMongoController;
462
+ }
463
+ });
464
+ exports.COLLECTION_DEF = COLLECTION_DEF
465
+ exports.CollectionController = CollectionController
@@ -0,0 +1,233 @@
1
+ import { HttpError } from '@moostjs/event-http';
2
+ import { TConsoleBase, Moost } from 'moost';
3
+ import { UrlqlQuery } from 'urlql';
4
+ import { AsMongo, AsCollection } from '@atscript/mongo';
5
+ import { TAtscriptAnnotatedTypeConstructor, ValidatorError } from '@atscript/typescript';
6
+ import { WithId, InsertOneResult, InsertManyResult, UpdateResult, DeleteResult, DeleteOptions, ObjectId, OptionalUnlessRequiredId, InsertOneOptions, BulkWriteOptions, WithoutId, ReplaceOptions, UpdateFilter, UpdateOptions } from 'mongodb';
7
+
8
+ /**
9
+ * Generic **Moost** controller that exposes a full REST‑style CRUD surface over a
10
+ * MongoDB collection described with **atscript** and handled by **@atscript/mongo**.
11
+ *
12
+ * The controller is intentionally designed for extension – simply subclass it and
13
+ * provide your model constructor. Every important step is overridable via the
14
+ * `protected` hooks documented below.
15
+ *
16
+ * ```ts
17
+ * ‎@Provide(AsMongo, () => new AsMongo(CONNECTION_STRING))
18
+ * ‎@CollectionController(MyCollectionType)
19
+ * export class MyCollectionController
20
+ * extends AsMongoController<typeof MyCollectionType> {}
21
+ * ```
22
+ *
23
+ * @typeParam T - The **atscript** annotated class (constructor) representing the
24
+ * collection schema. Must be decorated with `@AsCollection`.
25
+ */
26
+ declare class AsMongoController<T extends TAtscriptAnnotatedTypeConstructor> {
27
+ protected asMongo: AsMongo;
28
+ protected type: T;
29
+ /** Reference to the lazily created {@link AsCollection}. */
30
+ protected asCollection: AsCollection<T>;
31
+ /** Application‑scoped logger bound to the collection name. */
32
+ protected logger: TConsoleBase;
33
+ /**
34
+ * Creates a controller instance and resolves the underlying collection.
35
+ *
36
+ * > Do **not** perform heavy asynchronous work directly inside the
37
+ * > constructor – override {@link init} instead.
38
+ *
39
+ * @param asMongo - Shared `AsMongo` driver instance.
40
+ * @param type - AtScript annotated model constructor for the collection.
41
+ * @param app - The current `Moost` application (used for retrieving a logger).
42
+ * @throws Rethrows any error emitted from {@link init} to stop controller registration.
43
+ */
44
+ constructor(asMongo: AsMongo, type: T, app: Moost);
45
+ /**
46
+ * One‑time initialization hook executed right after the collection is obtained.
47
+ *
48
+ * Default behaviour:
49
+ * * Automatically synchronises MongoDB indexes unless
50
+ * `mongo.autoIndexes` metadata flag is set to `false` on the model.
51
+ *
52
+ * Override to seed data, register change streams, etc. Both sync and async
53
+ * return types are supported.
54
+ */
55
+ protected init(): void | Promise<any>;
56
+ private _queryControlsValidator?;
57
+ private _pagesControlsValidator?;
58
+ private _getOneControlsValidator?;
59
+ /** Returns (and memoises) validator for *query* endpoint controls. */
60
+ protected get queryControlsValidator(): any;
61
+ /** Returns (and memoises) validator for *pages* endpoint controls. */
62
+ protected get pagesControlsValidator(): any;
63
+ /** Returns (and memoises) validator for *one* endpoint controls. */
64
+ protected get getOneControlsValidator(): any;
65
+ /**
66
+ * Validates `$limit`, `$skip`, `$sort`, `$select`, `$count` controls for the
67
+ * **query** endpoint.
68
+ *
69
+ * @param controls - Controls object emitted by `urlql` parser.
70
+ * @returns Error message string or `undefined` (when valid). Can be async.
71
+ */
72
+ protected validateQueryControls(controls: UrlqlQuery['controls']): Promise<string | undefined> | string | undefined;
73
+ /**
74
+ * Validates pagination‑specific controls for the **pages** endpoint.
75
+ *
76
+ * @param controls - Controls object emitted by `urlql` parser.
77
+ * @returns Error message string or `undefined` (when valid). Can be async.
78
+ */
79
+ protected validatePagesControls(controls: UrlqlQuery['controls']): Promise<string | undefined> | string | undefined;
80
+ /**
81
+ * Validates controls for the **one /: id ** endpoint.
82
+ *
83
+ * @param controls - Controls object emitted by `urlql` parser.
84
+ * @returns Error message string or `undefined` (when valid). Can be async.
85
+ */
86
+ protected validateGetOneControls(controls: UrlqlQuery['controls']): Promise<string | undefined> | string | undefined;
87
+ /**
88
+ * Validates the `insights` section ensuring only known projection fields are used.
89
+ *
90
+ * @param insights - Map of insight keys.
91
+ * @returns Error message string or `undefined` (when valid). Can be async.
92
+ */
93
+ protected validateInsights(insights: UrlqlQuery['insights']): Promise<string | undefined> | string | undefined;
94
+ /**
95
+ * Runs all validations relevant to current endpoint and returns a ready‑to‑send
96
+ * `HttpError` instance if something is invalid.
97
+ *
98
+ * @param parsed - Full parsed URLQL query.
99
+ * @param controlsType - Which controls validator to apply.
100
+ */
101
+ protected validateUrlql(parsed: UrlqlQuery, controlsType: 'query' | 'pages' | 'getOne'): Promise<HttpError | undefined>;
102
+ /**
103
+ * Allows subclasses to translate field projection (e.g. whitelist vs blacklist).
104
+ *
105
+ * @param projection - Mongo projection generated by `urlql` `$select` control.
106
+ * @returns Adjusted projection (may return `Promise`).
107
+ */
108
+ protected transformProjection(projection?: Record<string, 0 | 1>): Record<string, 1> | Record<string, 0> | undefined | Promise<Record<string, 1> | Record<string, 0> | undefined>;
109
+ /**
110
+ * Builds MongoDB `FindOptions` object out of URLQL controls.
111
+ *
112
+ * @param controls - Parsed `controls` object.
113
+ */
114
+ protected prepareQueryOptions(controls: UrlqlQuery['controls']): {
115
+ projection: Record<string, 1> | Record<string, 0> | Promise<Record<string, 1> | Record<string, 0> | undefined> | undefined;
116
+ sort: Record<string, 1 | -1> | undefined;
117
+ limit: number | undefined;
118
+ skip: number | undefined;
119
+ };
120
+ /**
121
+ * **GET /query** – returns an array of documents or a count depending on
122
+ * presence of `$count` control.
123
+ *
124
+ * @param url - Full request URL provided by Moost (includes query string).
125
+ * @returns Documents array **or** document count number.
126
+ */
127
+ query(url: string): Promise<InstanceType<T>[] | number | HttpError>;
128
+ /**
129
+ * **GET /pages** – returns paginated documents plus basic pagination meta.
130
+ *
131
+ * @param url - Full request URL.
132
+ * @returns An object with keys: `documents`, `page`, `size`, `totalPages`, `totalDocuments`.
133
+ */
134
+ pages(url: string): Promise<{
135
+ documents: InstanceType<T>[];
136
+ page: number;
137
+ size: number;
138
+ totalPages: number;
139
+ totalDocuments: number;
140
+ } | HttpError>;
141
+ /**
142
+ * **GET /one/:id** – retrieves a single document. The identifier may be a
143
+ * Mongo `ObjectId` **or** the value of any `unique` property registered on
144
+ * the model.
145
+ *
146
+ * Filtering is not allowed on this route.
147
+ *
148
+ * @param id - Document `_id` or alternate unique key value.
149
+ * @param url - Full request URL (supports `$select`, `$insights`, etc.).
150
+ */
151
+ getOne(id: string, url: string): Promise<InstanceType<T> | HttpError | ValidatorError>;
152
+ /**
153
+ * Helper that unwraps a promise returning an array of documents and
154
+ * guarantees zero‑or‑one semantics expected by **one** endpoint.
155
+ *
156
+ * @param result - Promise resolving to an array of documents.
157
+ * @returns Document, 400 or 404 { @link HttpError }.
158
+ */
159
+ protected returnOne(result: Promise<WithId<InstanceType<T>>[]>): Promise<InstanceType<T> | HttpError>;
160
+ /**
161
+ * **POST /** – inserts one or many documents.
162
+ *
163
+ * @param payload - Raw request body to be inserted.
164
+ */
165
+ insert(payload: any): Promise<HttpError | InsertOneResult | InsertManyResult>;
166
+ /**
167
+ * **PUT /** – fully replaces a document matched by `_id`.
168
+ *
169
+ * @param payload - Object containing `_id` plus full replacement document.
170
+ */
171
+ replace(payload: any): Promise<HttpError | UpdateResult<InstanceType<T>>>;
172
+ /**
173
+ * **PATCH /** – updates one document using MongoDB update operators.
174
+ *
175
+ * @param payload - Update payload produced by `asCollection.prepareUpdate`.
176
+ */
177
+ update(payload: any): Promise<HttpError | UpdateResult>;
178
+ /**
179
+ * **DELETE /:id** – removes a single document by `_id`.
180
+ *
181
+ * @param id - Document identifier.
182
+ */
183
+ remove(id: string): Promise<HttpError | DeleteResult>;
184
+ /**
185
+ * Intercepts delete operation allowing subclasses to veto or mutate it.
186
+ *
187
+ * @param id - Requested document ID.
188
+ * @param opts - Mutable `DeleteOptions` passed to Mongo driver.
189
+ * @returns Final ID to be deleted (can be async).
190
+ */
191
+ protected onRemove(id: string, opts: DeleteOptions): string | number | Date | ObjectId | undefined | Promise<string | number | Date | ObjectId | undefined>;
192
+ /**
193
+ * Generic handler executed right before **every write** (insert/replace/update).
194
+ *
195
+ * Override to validate or mutate data, or tweak driver options. Return
196
+ * `undefined` to abort the write – the endpoint will respond with *500 Not saved*.
197
+ */
198
+ protected onWrite(action: 'insert', data: OptionalUnlessRequiredId<InstanceType<T>>, opts: InsertOneOptions): OptionalUnlessRequiredId<InstanceType<T>> | Promise<OptionalUnlessRequiredId<InstanceType<T>> | undefined> | undefined;
199
+ protected onWrite(action: 'insertMany', data: OptionalUnlessRequiredId<InstanceType<T>>[], opts: BulkWriteOptions): OptionalUnlessRequiredId<InstanceType<T>>[] | Promise<OptionalUnlessRequiredId<InstanceType<T>>[] | undefined> | undefined;
200
+ protected onWrite(action: 'replace', data: WithoutId<InstanceType<T>>, opts: ReplaceOptions): WithoutId<InstanceType<T>> | Promise<WithoutId<InstanceType<T>> | undefined> | undefined;
201
+ protected onWrite(action: 'update', data: UpdateFilter<InstanceType<T>>, opts: UpdateOptions): UpdateFilter<InstanceType<T>> | Promise<UpdateFilter<InstanceType<T>> | undefined> | undefined;
202
+ }
203
+
204
+ /**
205
+ * DI token under which the AtScript-annotated collection definition
206
+ * is exposed to the controller’s constructor via `@Inject`.
207
+ */
208
+ declare const COLLECTION_DEF = "__atscript_mongo_collection_def";
209
+ /**
210
+ * Combines the boilerplate needed to turn an {@link AsMongoController}
211
+ * subclass into a fully wired HTTP controller for a given
212
+ * **@mongo.collection** model.
213
+ *
214
+ * Internally applies three decorators:
215
+ * 1. **Provide** – registers the collection constructor under {@link COLLECTION_DEF}.
216
+ * 2. **Controller** – registers the class as a Moost HTTP controller
217
+ * with an optional route prefix. If the prefix is not set, the collection name used by default
218
+ * 3. **Inherit** – copies metadata (routes, guards, etc.) from the
219
+ * parent class so they stay active in the derived controller.
220
+ *
221
+ * @param type AtScript-annotated constructor produced by `@mongo.collection`.
222
+ * @param prefix Optional route prefix. Defaults to
223
+ * `type.metadata.get("mongo.collection")` or the class name.
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * ‎@CollectionController(UserModel)
228
+ * export class UsersController extends AsMongoController<typeof UserModel> {}
229
+ * ```
230
+ */
231
+ declare const CollectionController: (type: TAtscriptAnnotatedTypeConstructor, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
232
+
233
+ export { AsMongoController, COLLECTION_DEF, CollectionController };
package/dist/index.mjs ADDED
@@ -0,0 +1,434 @@
1
+ import { Body, Delete, Get, HttpError, Patch, Post, Put, Url } from "@moostjs/event-http";
2
+ import { ApplyDecorators, Controller, Inherit, Inject, Moost, Param, Provide } from "moost";
3
+ import { parseUrlql } from "urlql";
4
+ import { ValidatorError, defineAnnotatedType } from "@atscript/typescript";
5
+ import { AsMongo } from "@atscript/mongo";
6
+
7
+ //#region packages/moost-mongo/src/dto/controls.dto.as.js
8
+ function _define_property$1(obj, key, value) {
9
+ if (key in obj) Object.defineProperty(obj, key, {
10
+ value,
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true
14
+ });
15
+ else obj[key] = value;
16
+ return obj;
17
+ }
18
+ var QueryControlsDto = class {};
19
+ _define_property$1(QueryControlsDto, "__is_atscript_annotated_type", true);
20
+ _define_property$1(QueryControlsDto, "type", {});
21
+ _define_property$1(QueryControlsDto, "metadata", new Map());
22
+ var PagesControlsDto = class {};
23
+ _define_property$1(PagesControlsDto, "__is_atscript_annotated_type", true);
24
+ _define_property$1(PagesControlsDto, "type", {});
25
+ _define_property$1(PagesControlsDto, "metadata", new Map());
26
+ var GetOneControlsDto = class {};
27
+ _define_property$1(GetOneControlsDto, "__is_atscript_annotated_type", true);
28
+ _define_property$1(GetOneControlsDto, "type", {});
29
+ _define_property$1(GetOneControlsDto, "metadata", new Map());
30
+ let SortControlDto = class SortControlDto$1 {};
31
+ _define_property$1(SortControlDto, "__is_atscript_annotated_type", true);
32
+ _define_property$1(SortControlDto, "type", {});
33
+ _define_property$1(SortControlDto, "metadata", new Map());
34
+ let SelectControlDto = class SelectControlDto$1 {};
35
+ _define_property$1(SelectControlDto, "__is_atscript_annotated_type", true);
36
+ _define_property$1(SelectControlDto, "type", {});
37
+ _define_property$1(SelectControlDto, "metadata", new Map());
38
+ defineAnnotatedType("object", QueryControlsDto).prop("$skip", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.min", 0).annotate("expect.int", true).optional().$type).prop("$limit", defineAnnotatedType().designType("number").tags("positive", "int", "number").annotate("expect.min", 0).annotate("expect.int", true).optional().$type).prop("$count", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$select", defineAnnotatedType().refTo(SelectControlDto).optional().$type);
39
+ defineAnnotatedType("object", PagesControlsDto).prop("$page", defineAnnotatedType().designType("string").tags("string").annotate("expect.pattern", {
40
+ pattern: "^\\d+$",
41
+ flags: "u",
42
+ message: "Expected positive number"
43
+ }, true).optional().$type).prop("$size", defineAnnotatedType().designType("string").tags("string").annotate("expect.pattern", {
44
+ pattern: "^\\d+$",
45
+ flags: "u",
46
+ message: "Expected positive number"
47
+ }, true).optional().$type).prop("$sort", defineAnnotatedType().refTo(SortControlDto).optional().$type).prop("$select", defineAnnotatedType().refTo(SelectControlDto).optional().$type);
48
+ defineAnnotatedType("object", GetOneControlsDto).prop("$select", defineAnnotatedType().refTo(SelectControlDto).optional().$type);
49
+ defineAnnotatedType("object", SortControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("number").value(1).$type).item(defineAnnotatedType().designType("number").value(-1).$type).$type);
50
+ defineAnnotatedType("object", SelectControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("number").value(1).$type).item(defineAnnotatedType().designType("number").value(0).$type).$type);
51
+
52
+ //#endregion
53
+ //#region packages/moost-mongo/src/decorators.ts
54
+ const COLLECTION_DEF = "__atscript_mongo_collection_def";
55
+ const CollectionController = (type, prefix) => {
56
+ return ApplyDecorators(Provide(COLLECTION_DEF, () => type), Controller(prefix || type.metadata.get("mongo.collection") || type.name), Inherit());
57
+ };
58
+
59
+ //#endregion
60
+ //#region packages/moost-mongo/src/as-mongo.controller.ts
61
+ function _define_property(obj, key, value) {
62
+ if (key in obj) Object.defineProperty(obj, key, {
63
+ value,
64
+ enumerable: true,
65
+ configurable: true,
66
+ writable: true
67
+ });
68
+ else obj[key] = value;
69
+ return obj;
70
+ }
71
+ function _ts_decorate(decorators, target, key, desc) {
72
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
73
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
74
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
75
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
76
+ }
77
+ function _ts_metadata(k, v) {
78
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
79
+ }
80
+ function _ts_param(paramIndex, decorator) {
81
+ return function(target, key) {
82
+ decorator(target, key, paramIndex);
83
+ };
84
+ }
85
+ var AsMongoController = class {
86
+ /**
87
+ * One‑time initialization hook executed right after the collection is obtained.
88
+ *
89
+ * Default behaviour:
90
+ * * Automatically synchronises MongoDB indexes unless
91
+ * `mongo.autoIndexes` metadata flag is set to `false` on the model.
92
+ *
93
+ * Override to seed data, register change streams, etc. Both sync and async
94
+ * return types are supported.
95
+ */ init() {
96
+ if (this.type.metadata.get("mongo.autoIndexes") === false) {} else return this.asCollection.syncIndexes();
97
+ }
98
+ /** Returns (and memoises) validator for *query* endpoint controls. */ get queryControlsValidator() {
99
+ if (!this._queryControlsValidator) this._queryControlsValidator = QueryControlsDto.validator();
100
+ return this._queryControlsValidator;
101
+ }
102
+ /** Returns (and memoises) validator for *pages* endpoint controls. */ get pagesControlsValidator() {
103
+ if (!this._pagesControlsValidator) this._pagesControlsValidator = PagesControlsDto.validator();
104
+ return this._pagesControlsValidator;
105
+ }
106
+ /** Returns (and memoises) validator for *one* endpoint controls. */ get getOneControlsValidator() {
107
+ if (!this._getOneControlsValidator) this._getOneControlsValidator = GetOneControlsDto.validator();
108
+ return this._getOneControlsValidator;
109
+ }
110
+ /**
111
+ * Validates `$limit`, `$skip`, `$sort`, `$select`, `$count` controls for the
112
+ * **query** endpoint.
113
+ *
114
+ * @param controls - Controls object emitted by `urlql` parser.
115
+ * @returns Error message string or `undefined` (when valid). Can be async.
116
+ */ validateQueryControls(controls) {
117
+ this.queryControlsValidator.validate(controls);
118
+ return undefined;
119
+ }
120
+ /**
121
+ * Validates pagination‑specific controls for the **pages** endpoint.
122
+ *
123
+ * @param controls - Controls object emitted by `urlql` parser.
124
+ * @returns Error message string or `undefined` (when valid). Can be async.
125
+ */ validatePagesControls(controls) {
126
+ this.pagesControlsValidator.validate(controls);
127
+ return undefined;
128
+ }
129
+ /**
130
+ * Validates controls for the **one /: id ** endpoint.
131
+ *
132
+ * @param controls - Controls object emitted by `urlql` parser.
133
+ * @returns Error message string or `undefined` (when valid). Can be async.
134
+ */ validateGetOneControls(controls) {
135
+ this.getOneControlsValidator.validate(controls);
136
+ return undefined;
137
+ }
138
+ /**
139
+ * Validates the `insights` section ensuring only known projection fields are used.
140
+ *
141
+ * @param insights - Map of insight keys.
142
+ * @returns Error message string or `undefined` (when valid). Can be async.
143
+ */ validateInsights(insights) {
144
+ for (const key of insights.keys()) if (!this.asCollection.flatMap.has(key)) return `Unknown field "${key}"`;
145
+ return undefined;
146
+ }
147
+ /**
148
+ * Runs all validations relevant to current endpoint and returns a ready‑to‑send
149
+ * `HttpError` instance if something is invalid.
150
+ *
151
+ * @param parsed - Full parsed URLQL query.
152
+ * @param controlsType - Which controls validator to apply.
153
+ */ async validateUrlql(parsed, controlsType) {
154
+ const controlsValidators = {
155
+ query: this.validateQueryControls.bind(this),
156
+ pages: this.validatePagesControls.bind(this),
157
+ getOne: this.validateGetOneControls.bind(this)
158
+ };
159
+ try {
160
+ const error = await controlsValidators[controlsType](parsed.controls);
161
+ if (error) return new HttpError(400, error);
162
+ } catch (e) {
163
+ return new HttpError(400, e.message);
164
+ }
165
+ try {
166
+ const error = await this.validateInsights(parsed.insights);
167
+ if (error) return new HttpError(400, error);
168
+ } catch (e) {
169
+ return new HttpError(400, e.message);
170
+ }
171
+ }
172
+ /**
173
+ * Allows subclasses to translate field projection (e.g. whitelist vs blacklist).
174
+ *
175
+ * @param projection - Mongo projection generated by `urlql` `$select` control.
176
+ * @returns Adjusted projection (may return `Promise`).
177
+ */ transformProjection(projection) {
178
+ return projection;
179
+ }
180
+ /**
181
+ * Builds MongoDB `FindOptions` object out of URLQL controls.
182
+ *
183
+ * @param controls - Parsed `controls` object.
184
+ */ prepareQueryOptions(controls) {
185
+ return {
186
+ projection: this.transformProjection(controls.$select),
187
+ sort: controls.$sort,
188
+ limit: controls.$limit,
189
+ skip: controls.$skip
190
+ };
191
+ }
192
+ /**
193
+ * **GET /query** – returns an array of documents or a count depending on
194
+ * presence of `$count` control.
195
+ *
196
+ * @param url - Full request URL provided by Moost (includes query string).
197
+ * @returns Documents array **or** document count number.
198
+ */ async query(url) {
199
+ const query = url.split("?").slice(1).join("?");
200
+ const parsed = parseUrlql(query);
201
+ const error = await this.validateUrlql(parsed, "query");
202
+ if (error) return error;
203
+ return parsed.controls.$count ? this.asCollection.collection.countDocuments(parsed.filter) : this.asCollection.collection.find(parsed.filter, this.prepareQueryOptions(parsed.controls)).toArray();
204
+ }
205
+ /**
206
+ * **GET /pages** – returns paginated documents plus basic pagination meta.
207
+ *
208
+ * @param url - Full request URL.
209
+ * @returns An object with keys: `documents`, `page`, `size`, `totalPages`, `totalDocuments`.
210
+ */ async pages(url) {
211
+ const query = url.split("?").slice(1).join("?");
212
+ const parsed = parseUrlql(query);
213
+ const error = await this.validateUrlql(parsed, "pages");
214
+ if (error) return error;
215
+ const controls = parsed.controls;
216
+ const page = Math.max(Number(controls.$page || 1), 1);
217
+ const size = Math.max(Number(controls.$size || 10), 1);
218
+ const skip = (page - 1) * size;
219
+ const result = await this.asCollection.collection.aggregate([{ $match: parsed.filter }, { $facet: {
220
+ documents: [
221
+ controls.$sort ? { $sort: controls.$sort } : undefined,
222
+ { $skip: skip },
223
+ { $limit: size },
224
+ controls.$select ? { $project: controls.$select } : undefined
225
+ ].filter(Boolean),
226
+ meta: [{ $count: "count" }]
227
+ } }]).toArray();
228
+ const totalDocuments = result[0].meta[0].count;
229
+ return {
230
+ documents: result[0].documents,
231
+ page,
232
+ size,
233
+ totalPages: Math.ceil(totalDocuments / size),
234
+ totalDocuments
235
+ };
236
+ }
237
+ /**
238
+ * **GET /one/:id** – retrieves a single document. The identifier may be a
239
+ * Mongo `ObjectId` **or** the value of any `unique` property registered on
240
+ * the model.
241
+ *
242
+ * Filtering is not allowed on this route.
243
+ *
244
+ * @param id - Document `_id` or alternate unique key value.
245
+ * @param url - Full request URL (supports `$select`, `$insights`, etc.).
246
+ */ async getOne(id, url) {
247
+ const idValidator = this.asCollection.flatMap.get("_id")?.validator();
248
+ const query = url.split("?").slice(1).join("?");
249
+ const parsed = parseUrlql(query);
250
+ if (Object.keys(parsed.filter).length) return new HttpError(400, "Filtering is not allowed for \"one\" endpoint");
251
+ const error = await this.validateUrlql(parsed, "getOne");
252
+ if (error) return error;
253
+ if (idValidator?.validate(id, true)) return this.returnOne(this.asCollection.collection.find({ _id: this.asCollection.prepareId(id) }, this.prepareQueryOptions(parsed.controls)).toArray());
254
+ else if (this.asCollection.uniqueProps.size > 0) {
255
+ const filter = [];
256
+ for (const prop of this.asCollection.uniqueProps) filter.push({ [prop]: id });
257
+ return this.returnOne(this.asCollection.collection.find({ $or: filter }, this.prepareQueryOptions(parsed.controls)).toArray());
258
+ }
259
+ if (idValidator) return new ValidatorError(idValidator.errors);
260
+ return new HttpError(500, "Unknown error");
261
+ }
262
+ /**
263
+ * Helper that unwraps a promise returning an array of documents and
264
+ * guarantees zero‑or‑one semantics expected by **one** endpoint.
265
+ *
266
+ * @param result - Promise resolving to an array of documents.
267
+ * @returns Document, 400 or 404 { @link HttpError }.
268
+ */ async returnOne(result) {
269
+ const items = await result;
270
+ if (items.length > 1) return new HttpError(400, "Found more than one record");
271
+ else if (items.length === 0) return new HttpError(404);
272
+ else return items[0];
273
+ }
274
+ /**
275
+ * **POST /** – inserts one or many documents.
276
+ *
277
+ * @param payload - Raw request body to be inserted.
278
+ */ async insert(payload) {
279
+ const data = this.asCollection.prepareInsert(payload);
280
+ const opts = {};
281
+ if (Array.isArray(data)) {
282
+ const newData = await this.onWrite("insertMany", data, opts);
283
+ if (newData) return this.asCollection.collection.insertMany(newData, opts);
284
+ else return new HttpError(500, "Not saved");
285
+ }
286
+ if (data) {
287
+ const newData = await this.onWrite("insert", data, opts);
288
+ if (newData) return this.asCollection.collection.insertOne(newData, opts);
289
+ else return new HttpError(500, "Not saved");
290
+ }
291
+ return new HttpError(500, "Not saved");
292
+ }
293
+ /**
294
+ * **PUT /** – fully replaces a document matched by `_id`.
295
+ *
296
+ * @param payload - Object containing `_id` plus full replacement document.
297
+ */ async replace(payload) {
298
+ const args = this.asCollection.prepareReplace(payload).toArgs();
299
+ const newData = await this.onWrite("replace", args[1], args[2]);
300
+ if (newData) return this.asCollection.collection.replaceOne(args[0], newData, args[2]);
301
+ return new HttpError(500, "Not saved");
302
+ }
303
+ /**
304
+ * **PATCH /** – updates one document using MongoDB update operators.
305
+ *
306
+ * @param payload - Update payload produced by `asCollection.prepareUpdate`.
307
+ */ async update(payload) {
308
+ const args = this.asCollection.prepareUpdate(payload).toArgs();
309
+ const newData = await this.onWrite("update", args[1], args[2]);
310
+ if (newData) return this.asCollection.collection.updateOne(args[0], newData, args[2]);
311
+ return new HttpError(500, "Not saved");
312
+ }
313
+ /**
314
+ * **DELETE /:id** – removes a single document by `_id`.
315
+ *
316
+ * @param id - Document identifier.
317
+ */ async remove(id) {
318
+ const opts = {};
319
+ id = await this.onRemove(id, opts);
320
+ if (id !== undefined) {
321
+ const result = await this.asCollection.collection.deleteOne({ _id: this.asCollection.prepareId(id) }, opts);
322
+ if (result.deletedCount < 1) throw new HttpError(404);
323
+ return result;
324
+ }
325
+ return new HttpError(500, "Not deleted");
326
+ }
327
+ /**
328
+ * Intercepts delete operation allowing subclasses to veto or mutate it.
329
+ *
330
+ * @param id - Requested document ID.
331
+ * @param opts - Mutable `DeleteOptions` passed to Mongo driver.
332
+ * @returns Final ID to be deleted (can be async).
333
+ */ onRemove(id, opts) {
334
+ return id;
335
+ }
336
+ onWrite(action, data, opts) {
337
+ return data;
338
+ }
339
+ /**
340
+ * Creates a controller instance and resolves the underlying collection.
341
+ *
342
+ * > Do **not** perform heavy asynchronous work directly inside the
343
+ * > constructor – override {@link init} instead.
344
+ *
345
+ * @param asMongo - Shared `AsMongo` driver instance.
346
+ * @param type - AtScript annotated model constructor for the collection.
347
+ * @param app - The current `Moost` application (used for retrieving a logger).
348
+ * @throws Rethrows any error emitted from {@link init} to stop controller registration.
349
+ */ constructor(asMongo, type, app) {
350
+ _define_property(this, "asMongo", void 0);
351
+ _define_property(this, "type", void 0);
352
+ /** Reference to the lazily created {@link AsCollection}. */ _define_property(this, "asCollection", void 0);
353
+ /** Application‑scoped logger bound to the collection name. */ _define_property(this, "logger", void 0);
354
+ _define_property(this, "_queryControlsValidator", void 0);
355
+ _define_property(this, "_pagesControlsValidator", void 0);
356
+ _define_property(this, "_getOneControlsValidator", void 0);
357
+ this.asMongo = asMongo;
358
+ this.type = type;
359
+ this.logger = app.getLogger(`mongo [${type.metadata.get("mongo.collection") || ""}]`);
360
+ this.asCollection = this.asMongo.getCollection(type, this.logger);
361
+ this.logger.info(`Initializing Collection`);
362
+ try {
363
+ const p = this.init();
364
+ if (p instanceof Promise) p.catch((e) => {
365
+ this.logger.error(e);
366
+ });
367
+ } catch (e) {
368
+ this.logger.error(e);
369
+ throw e;
370
+ }
371
+ }
372
+ };
373
+ _ts_decorate([
374
+ Get("query"),
375
+ _ts_param(0, Url()),
376
+ _ts_metadata("design:type", Function),
377
+ _ts_metadata("design:paramtypes", [String]),
378
+ _ts_metadata("design:returntype", Promise)
379
+ ], AsMongoController.prototype, "query", null);
380
+ _ts_decorate([
381
+ Get("pages"),
382
+ _ts_param(0, Url()),
383
+ _ts_metadata("design:type", Function),
384
+ _ts_metadata("design:paramtypes", [String]),
385
+ _ts_metadata("design:returntype", Promise)
386
+ ], AsMongoController.prototype, "pages", null);
387
+ _ts_decorate([
388
+ Get("one/:id"),
389
+ _ts_param(0, Param("id")),
390
+ _ts_param(1, Url()),
391
+ _ts_metadata("design:type", Function),
392
+ _ts_metadata("design:paramtypes", [String, String]),
393
+ _ts_metadata("design:returntype", Promise)
394
+ ], AsMongoController.prototype, "getOne", null);
395
+ _ts_decorate([
396
+ Post(""),
397
+ _ts_param(0, Body()),
398
+ _ts_metadata("design:type", Function),
399
+ _ts_metadata("design:paramtypes", [Object]),
400
+ _ts_metadata("design:returntype", Promise)
401
+ ], AsMongoController.prototype, "insert", null);
402
+ _ts_decorate([
403
+ Put(""),
404
+ _ts_param(0, Body()),
405
+ _ts_metadata("design:type", Function),
406
+ _ts_metadata("design:paramtypes", [Object]),
407
+ _ts_metadata("design:returntype", Promise)
408
+ ], AsMongoController.prototype, "replace", null);
409
+ _ts_decorate([
410
+ Patch(""),
411
+ _ts_param(0, Body()),
412
+ _ts_metadata("design:type", Function),
413
+ _ts_metadata("design:paramtypes", [Object]),
414
+ _ts_metadata("design:returntype", Promise)
415
+ ], AsMongoController.prototype, "update", null);
416
+ _ts_decorate([
417
+ Delete(":id"),
418
+ _ts_param(0, Param("id")),
419
+ _ts_metadata("design:type", Function),
420
+ _ts_metadata("design:paramtypes", [String]),
421
+ _ts_metadata("design:returntype", Promise)
422
+ ], AsMongoController.prototype, "remove", null);
423
+ AsMongoController = _ts_decorate([
424
+ _ts_param(1, Inject(COLLECTION_DEF)),
425
+ _ts_metadata("design:type", Function),
426
+ _ts_metadata("design:paramtypes", [
427
+ typeof AsMongo === "undefined" ? Object : AsMongo,
428
+ typeof T === "undefined" ? Object : T,
429
+ typeof Moost === "undefined" ? Object : Moost
430
+ ])
431
+ ], AsMongoController);
432
+
433
+ //#endregion
434
+ export { AsMongoController, COLLECTION_DEF, CollectionController };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@atscript/moost-mongo",
3
+ "version": "0.0.18",
4
+ "description": "Atscript Mongo for Moost.",
5
+ "type": "module",
6
+ "main": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "keywords": [
20
+ "atscript",
21
+ "annotations",
22
+ "typescript",
23
+ "moost",
24
+ "mongo"
25
+ ],
26
+ "author": "Artem Maltsev",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/moostjs/atscript.git",
30
+ "directory": "packages/moost-mongo"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/moostjs/atscript/issues"
34
+ },
35
+ "homepage": "https://github.com/moostjs/atscript/tree/main/packages/moost-mongo#readme",
36
+ "license": "ISC",
37
+ "dependencies": {
38
+ "urlql": "^0.0.4"
39
+ },
40
+ "devDependencies": {
41
+ "vitest": "3.2.4"
42
+ },
43
+ "peerDependencies": {
44
+ "@moostjs/event-http": "^0.5.30",
45
+ "mongodb": "^6.17.0",
46
+ "moost": "^0.5.30",
47
+ "@atscript/mongo": "^0.0.18",
48
+ "@atscript/typescript": "^0.0.18"
49
+ },
50
+ "scripts": {
51
+ "pub": "pnpm publish --access public",
52
+ "before-build": "node ../typescript/cli.cjs -f js",
53
+ "test": "vitest"
54
+ }
55
+ }