@airoom/nextmin-node 0.1.0

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.
Files changed (45) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +178 -0
  3. package/dist/api/apiRouter.d.ts +65 -0
  4. package/dist/api/apiRouter.js +1548 -0
  5. package/dist/database/DatabaseAdapter.d.ts +14 -0
  6. package/dist/database/DatabaseAdapter.js +2 -0
  7. package/dist/database/InMemoryAdapter.d.ts +15 -0
  8. package/dist/database/InMemoryAdapter.js +71 -0
  9. package/dist/database/MongoAdapter.d.ts +52 -0
  10. package/dist/database/MongoAdapter.js +409 -0
  11. package/dist/files/FileStorageAdapter.d.ts +35 -0
  12. package/dist/files/FileStorageAdapter.js +2 -0
  13. package/dist/files/S3FileStorageAdapter.d.ts +30 -0
  14. package/dist/files/S3FileStorageAdapter.js +84 -0
  15. package/dist/files/filename.d.ts +5 -0
  16. package/dist/files/filename.js +40 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +87 -0
  19. package/dist/models/BaseModel.d.ts +44 -0
  20. package/dist/models/BaseModel.js +31 -0
  21. package/dist/policy/authorize.d.ts +25 -0
  22. package/dist/policy/authorize.js +305 -0
  23. package/dist/policy/conditions.d.ts +14 -0
  24. package/dist/policy/conditions.js +30 -0
  25. package/dist/policy/types.d.ts +53 -0
  26. package/dist/policy/types.js +2 -0
  27. package/dist/policy/utils.d.ts +9 -0
  28. package/dist/policy/utils.js +118 -0
  29. package/dist/schemas/Roles.json +64 -0
  30. package/dist/schemas/Settings.json +62 -0
  31. package/dist/schemas/Users.json +123 -0
  32. package/dist/services/SchemaService.d.ts +10 -0
  33. package/dist/services/SchemaService.js +46 -0
  34. package/dist/utils/DefaultDataInitializer.d.ts +21 -0
  35. package/dist/utils/DefaultDataInitializer.js +269 -0
  36. package/dist/utils/Logger.d.ts +12 -0
  37. package/dist/utils/Logger.js +79 -0
  38. package/dist/utils/SchemaLoader.d.ts +51 -0
  39. package/dist/utils/SchemaLoader.js +323 -0
  40. package/dist/utils/apiKey.d.ts +5 -0
  41. package/dist/utils/apiKey.js +14 -0
  42. package/dist/utils/fieldCodecs.d.ts +13 -0
  43. package/dist/utils/fieldCodecs.js +133 -0
  44. package/package.json +45 -0
  45. package/tsconfig.json +8 -0
@@ -0,0 +1,14 @@
1
+ import { ReadOptions, Schema } from '../models/BaseModel';
2
+ export type IndexDir = 1 | -1;
3
+ export type FieldIndexSpec = Record<string, IndexDir>;
4
+ export interface DatabaseAdapter {
5
+ connect(): Promise<void>;
6
+ disconnect(): Promise<void>;
7
+ registerSchemas(schemas: Record<string, Schema>): Promise<void> | void;
8
+ create(collection: string, data: any, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<any>;
9
+ read(collection: string, query: any, limit?: number, skip?: number, schemaDefinition?: Schema, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
10
+ update(collection: string, id: string, data: any, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<any>;
11
+ delete(collection: string, id: string, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<any>;
12
+ count(collection: string, query: any, schemaDefinition: Schema, includePrivateFields?: boolean): Promise<number>;
13
+ syncIndexes?(modelName: string, desired: FieldIndexSpec): Promise<void>;
14
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,15 @@
1
+ import { DatabaseAdapter, FieldIndexSpec } from './DatabaseAdapter';
2
+ import { ReadOptions, Schema } from '../models/BaseModel';
3
+ export declare class InMemoryAdapter implements DatabaseAdapter {
4
+ private data;
5
+ connect(): Promise<void>;
6
+ disconnect(): Promise<void>;
7
+ registerSchemas(schemas: Record<string, Schema>): void;
8
+ create(collection: string, data: any, schemaDefinition: Schema): Promise<any>;
9
+ read(collection: string, query: any, limit?: number, skip?: number, schemaDefinition?: Schema, _includePrivateFields?: boolean, _options?: ReadOptions): Promise<any[]>;
10
+ update(collection: string, id: string, data: any, schemaDefinition: Schema): Promise<any>;
11
+ delete(collection: string, id: string, schemaDefinition: Schema): Promise<any>;
12
+ count(collection: string, query: any, schemaDefinition: Schema): Promise<number>;
13
+ syncIndexes(_modelName: string, _desired: FieldIndexSpec): Promise<void>;
14
+ private generateId;
15
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryAdapter = void 0;
4
+ class InMemoryAdapter {
5
+ constructor() {
6
+ this.data = {};
7
+ }
8
+ async connect() {
9
+ // No-op for in-memory
10
+ }
11
+ async disconnect() {
12
+ // No-op for in-memory
13
+ }
14
+ registerSchemas(schemas) {
15
+ // No-op for in-memory, but you could validate schemas here if needed
16
+ }
17
+ async create(collection, data, schemaDefinition) {
18
+ if (!this.data[collection]) {
19
+ this.data[collection] = [];
20
+ }
21
+ // Generate a simple id if not present
22
+ if (!data.id) {
23
+ data.id = `${Date.now()}_${Math.random()}`;
24
+ }
25
+ this.data[collection].push(data);
26
+ return data;
27
+ }
28
+ async read(collection, query, limit, skip, schemaDefinition, _includePrivateFields, _options) {
29
+ const allData = this.data[collection] || [];
30
+ let results = allData.filter((item) => {
31
+ return Object.entries(query).every(([key, value]) => item[key] === value);
32
+ });
33
+ if (limit !== undefined && skip !== undefined) {
34
+ results = results.slice(skip, skip + limit);
35
+ }
36
+ return results;
37
+ }
38
+ async update(collection, id, data, schemaDefinition) {
39
+ const allData = this.data[collection] || [];
40
+ const index = allData.findIndex((item) => item.id === id);
41
+ if (index === -1) {
42
+ throw new Error(`Document with id ${id} not found in collection '${collection}'`);
43
+ }
44
+ this.data[collection][index] = { ...allData[index], ...data };
45
+ return this.data[collection][index];
46
+ }
47
+ async delete(collection, id, schemaDefinition) {
48
+ const allData = this.data[collection] || [];
49
+ const index = allData.findIndex((item) => item.id === id);
50
+ if (index === -1) {
51
+ throw new Error(`Document with id ${id} not found in collection '${collection}'`);
52
+ }
53
+ const deleted = this.data[collection].splice(index, 1)[0];
54
+ return deleted;
55
+ }
56
+ async count(collection, query, schemaDefinition) {
57
+ const allData = this.data[collection] || [];
58
+ // Count documents matching the query
59
+ const results = allData.filter((item) => {
60
+ return Object.entries(query).every(([key, value]) => item[key] === value);
61
+ });
62
+ return results.length;
63
+ }
64
+ async syncIndexes(_modelName, _desired) {
65
+ // no-op for memory adapter
66
+ }
67
+ generateId() {
68
+ return Math.random().toString(36).substr(2, 9);
69
+ }
70
+ }
71
+ exports.InMemoryAdapter = InMemoryAdapter;
@@ -0,0 +1,52 @@
1
+ import { DatabaseAdapter, FieldIndexSpec } from './DatabaseAdapter';
2
+ import { ReadOptions, Schema as NextMinSchema } from '../models/BaseModel';
3
+ export declare class MongoAdapter implements DatabaseAdapter {
4
+ private url;
5
+ private dbName;
6
+ private connection;
7
+ /** Map keyed by lowercased modelName (e.g., "users", "roles") → Mongoose Model */
8
+ private models;
9
+ constructor(url: string, dbName: string);
10
+ connect(): Promise<void>;
11
+ disconnect(): Promise<void>;
12
+ /**
13
+ * Register (and re-register) all schemas.
14
+ * - Drops compiled models that no longer exist.
15
+ * - Recompiles every current model so validators reflect latest schema.
16
+ */
17
+ registerSchemas(schemas: Record<string, NextMinSchema>): Promise<void>;
18
+ /** Optional hook used by the router when schemas are removed. */
19
+ unregisterSchemas(names: string[]): Promise<void>;
20
+ /** Optional single-model drop hook. */
21
+ dropModel(nameRaw: string): Promise<void>;
22
+ /**
23
+ * Delete a compiled model by case-insensitive match.
24
+ * Works whether we compiled it as "Users" or "users".
25
+ */
26
+ private safeDeleteModel;
27
+ private safeDropCollection;
28
+ /** Build a fresh Mongoose schema from our NextMin schema definition. */
29
+ private buildMongooseSchema;
30
+ private mapAttribute;
31
+ private mapScalar;
32
+ /**
33
+ * Resolve (or lazily create) the compiled model for a schema.
34
+ * Uses schema.modelName as the Mongoose model name and
35
+ * (schema.collection ?? modelName).toLowerCase() as the collection.
36
+ */
37
+ private getModel;
38
+ private getSchema;
39
+ private convertQueryFieldsToObjectId;
40
+ private toAppObject;
41
+ create(collection: string, data: any, schemaDefinition: NextMinSchema, includePrivateFields?: boolean): Promise<any>;
42
+ read(collection: string, query: any, limit: number | undefined, skip: number | undefined, schemaDefinition: NextMinSchema, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
43
+ count(collection: string, query: any, schemaDefinition: NextMinSchema): Promise<number>;
44
+ update(collection: string, id: string, data: any, schemaDefinition: NextMinSchema, includePrivateFields?: boolean): Promise<any>;
45
+ delete(collection: string, id: string, schemaDefinition: NextMinSchema, includePrivateFields?: boolean): Promise<any>;
46
+ private getNativeCollectionByModelName;
47
+ /** Create/drop single-field indexes based on schema plan.
48
+ * Only touches indexes named with the prefix below (won't touch user indexes).
49
+ */
50
+ private managedIndexName;
51
+ syncIndexes(modelName: string, desired: FieldIndexSpec): Promise<void>;
52
+ }
@@ -0,0 +1,409 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.MongoAdapter = void 0;
40
+ const mongoose_1 = __importStar(require("mongoose"));
41
+ const mongoose_autopopulate_1 = __importDefault(require("mongoose-autopopulate"));
42
+ const Logger_1 = __importDefault(require("../utils/Logger"));
43
+ const SchemaLoader_1 = require("../utils/SchemaLoader");
44
+ // Prevent Mongoose from pluralizing model names automatically
45
+ mongoose_1.default.pluralize(null);
46
+ class MongoAdapter {
47
+ constructor(url, dbName) {
48
+ this.url = url;
49
+ this.dbName = dbName;
50
+ this.connection = mongoose_1.default.createConnection(`${this.url}/${this.dbName}`);
51
+ this.models = new Map();
52
+ }
53
+ async connect() {
54
+ await this.connection.asPromise();
55
+ Logger_1.default.info('MongoAdapter', `Connected to MongoDB database: ${this.dbName}`);
56
+ }
57
+ async disconnect() {
58
+ await this.connection.close();
59
+ Logger_1.default.warn('MongoAdapter', 'Disconnected from MongoDB database');
60
+ }
61
+ /**
62
+ * Register (and re-register) all schemas.
63
+ * - Drops compiled models that no longer exist.
64
+ * - Recompiles every current model so validators reflect latest schema.
65
+ */
66
+ async registerSchemas(schemas) {
67
+ // Build the incoming set from schema.modelName (not object keys!)
68
+ const incoming = new Set(Object.values(schemas).map((s) => s.modelName.toLowerCase()));
69
+ // 1) Remove compiled models that are no longer present
70
+ for (const name of Array.from(this.models.keys())) {
71
+ if (!incoming.has(name)) {
72
+ this.safeDeleteModel(name);
73
+ this.models.delete(name);
74
+ // Optional: also drop the collection (disabled by default)
75
+ // await this.safeDropCollection(name);
76
+ }
77
+ }
78
+ // 2) (Re)compile every current model so validators and fields are fresh
79
+ for (const def of Object.values(schemas)) {
80
+ const modelKey = def.modelName; // e.g. "Users"
81
+ const modelKeyLc = modelKey.toLowerCase();
82
+ const collection = def.collection?.toLowerCase() ?? modelKeyLc; // e.g. "users"
83
+ // always delete previous compiled model to avoid OverwriteModelError and stale validators
84
+ this.safeDeleteModel(modelKey);
85
+ const mSchema = this.buildMongooseSchema(def);
86
+ const model = this.connection.model(modelKey, mSchema, collection);
87
+ this.models.set(modelKeyLc, model);
88
+ }
89
+ }
90
+ /** Optional hook used by the router when schemas are removed. */
91
+ async unregisterSchemas(names) {
92
+ for (const raw of names) {
93
+ const lc = raw.toLowerCase();
94
+ this.safeDeleteModel(lc);
95
+ this.models.delete(lc);
96
+ // Optional: await this.safeDropCollection(lc);
97
+ }
98
+ }
99
+ /** Optional single-model drop hook. */
100
+ async dropModel(nameRaw) {
101
+ const lc = nameRaw.toLowerCase();
102
+ this.safeDeleteModel(lc);
103
+ this.models.delete(lc);
104
+ // Optional: await this.safeDropCollection(lc);
105
+ }
106
+ // ---------------- Internals ----------------
107
+ /**
108
+ * Delete a compiled model by case-insensitive match.
109
+ * Works whether we compiled it as "Users" or "users".
110
+ */
111
+ safeDeleteModel(nameOrLc) {
112
+ try {
113
+ const targetLc = nameOrLc.toLowerCase();
114
+ const keys = Object.keys(this.connection.models);
115
+ for (const k of keys) {
116
+ if (k.toLowerCase() === targetLc) {
117
+ this.connection.deleteModel(k);
118
+ }
119
+ }
120
+ }
121
+ catch {
122
+ /* ignore */
123
+ }
124
+ }
125
+ async safeDropCollection(name) {
126
+ try {
127
+ await this.connection.dropCollection(name);
128
+ }
129
+ catch (e) {
130
+ // ignore "ns not found" etc.
131
+ if (e?.codeName !== 'NamespaceNotFound') {
132
+ Logger_1.default.warn('MongoAdapter', `dropCollection(${name}) warning:`, e?.message || e);
133
+ }
134
+ }
135
+ }
136
+ /** Build a fresh Mongoose schema from our NextMin schema definition. */
137
+ buildMongooseSchema(def) {
138
+ const shape = {};
139
+ for (const [key, attr] of Object.entries(def.attributes)) {
140
+ shape[key] = this.mapAttribute(attr);
141
+ }
142
+ const s = new mongoose_1.Schema(shape, { timestamps: true });
143
+ // Hide private fields by default when toObject() is used (unless overridden)
144
+ s.set('toObject', {
145
+ transform: (_doc, ret) => {
146
+ for (const [key, attr] of Object.entries(def.attributes)) {
147
+ if (!Array.isArray(attr) && attr.private) {
148
+ delete ret[key];
149
+ }
150
+ }
151
+ return ret;
152
+ },
153
+ });
154
+ s.plugin(mongoose_autopopulate_1.default);
155
+ return s;
156
+ }
157
+ mapAttribute(attribute) {
158
+ // Arrays
159
+ if (Array.isArray(attribute)) {
160
+ const elem = attribute[0] || { type: 'string' };
161
+ const mapped = this.mapScalar(elem);
162
+ // Mongoose "array of refs" works best as [{ type: X, ref: 'Y', autopopulate: true }]
163
+ const arrItem = { ...mapped };
164
+ // required belongs on the path, not the inner type
165
+ const required = !!elem.required;
166
+ delete arrItem.required;
167
+ return { type: [arrItem], required };
168
+ }
169
+ // Single
170
+ return this.mapScalar(attribute);
171
+ }
172
+ mapScalar(attr) {
173
+ const typeMapping = {
174
+ string: String,
175
+ number: Number,
176
+ boolean: Boolean,
177
+ date: Date,
178
+ array: Array,
179
+ object: Object,
180
+ objectid: mongoose_1.default.Schema.Types.ObjectId,
181
+ };
182
+ const out = {
183
+ type: typeMapping[String(attr?.type || 'string').toLowerCase()] || String,
184
+ };
185
+ // Only set when truthy to avoid forcing behavior when unset
186
+ if (attr?.required === true)
187
+ out.required = true;
188
+ if (attr?.unique === true)
189
+ out.unique = true;
190
+ if (attr?.ref) {
191
+ out.ref = attr.ref;
192
+ out.autopopulate = true;
193
+ }
194
+ if (attr?.default !== undefined) {
195
+ if (String(attr.type).toLowerCase() === 'date' &&
196
+ attr.default === 'now') {
197
+ out.default = Date.now;
198
+ }
199
+ else {
200
+ out.default = attr.default;
201
+ }
202
+ }
203
+ // Optional common validators
204
+ if (attr?.enum)
205
+ out.enum = attr.enum;
206
+ if (attr?.min !== undefined)
207
+ out.min = attr.min;
208
+ if (attr?.max !== undefined)
209
+ out.max = attr.max;
210
+ if (attr?.minLength !== undefined)
211
+ out.minLength = attr.minLength;
212
+ if (attr?.maxLength !== undefined)
213
+ out.maxLength = attr.maxLength;
214
+ if (attr?.match)
215
+ out.match = new RegExp(attr.match);
216
+ return out;
217
+ }
218
+ /**
219
+ * Resolve (or lazily create) the compiled model for a schema.
220
+ * Uses schema.modelName as the Mongoose model name and
221
+ * (schema.collection ?? modelName).toLowerCase() as the collection.
222
+ */
223
+ getModel(collection, schemaDefinition) {
224
+ const modelKey = schemaDefinition.modelName; // e.g. "Users"
225
+ const modelKeyLc = modelKey.toLowerCase();
226
+ const collectionName = schemaDefinition.collection?.toLowerCase() ??
227
+ collection.toLowerCase();
228
+ // Prefer cached by lowercased model key
229
+ const cached = this.models.get(modelKeyLc);
230
+ if (cached)
231
+ return cached;
232
+ // Ensure any dependency (rare) – preserved from your original example
233
+ if (schemaDefinition.attributes.author?.ref === 'Users') {
234
+ const userSchema = this.getSchema('Users');
235
+ if (!userSchema)
236
+ throw new Error(`Missing schema for Users`);
237
+ if (!this.models.has('users')) {
238
+ const uModel = this.connection.model(userSchema.modelName, this.buildMongooseSchema(userSchema), userSchema.collection?.toLowerCase() ??
239
+ userSchema.modelName.toLowerCase());
240
+ this.models.set('users', uModel);
241
+ }
242
+ }
243
+ // Remove any stale compiled model(s) with same name (case-insensitive), then (re)create
244
+ this.safeDeleteModel(modelKey);
245
+ const model = this.connection.model(modelKey, this.buildMongooseSchema(schemaDefinition), collectionName);
246
+ this.models.set(modelKeyLc, model);
247
+ return model;
248
+ }
249
+ getSchema(modelName) {
250
+ // Use the singleton loader so we read the same live map as the router
251
+ const loader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
252
+ const schemas = loader.getSchemas();
253
+ // The loader returns a map keyed by modelName; prefer lookup by value
254
+ const byName = Object.values(schemas).find((s) => String(s.modelName).toLowerCase() === modelName.toLowerCase());
255
+ return byName;
256
+ }
257
+ convertQueryFieldsToObjectId(query, schemaDefinition) {
258
+ const newQuery = { ...query };
259
+ // Remap 'id' to '_id'
260
+ if (newQuery.id !== undefined) {
261
+ try {
262
+ newQuery._id = new mongoose_1.default.Types.ObjectId(String(newQuery.id));
263
+ }
264
+ catch {
265
+ // leave as-is if not a valid ObjectId
266
+ newQuery._id = newQuery.id;
267
+ }
268
+ delete newQuery.id;
269
+ }
270
+ // Convert any fields defined as objectid
271
+ for (const [key, attribute] of Object.entries(schemaDefinition.attributes)) {
272
+ if (!Array.isArray(attribute) &&
273
+ String(attribute.type || '').toLowerCase() === 'objectid') {
274
+ const val = newQuery[key];
275
+ if (val === undefined || val === null)
276
+ continue;
277
+ const toObjectId = (v) => {
278
+ try {
279
+ return new mongoose_1.default.Types.ObjectId(String(v));
280
+ }
281
+ catch {
282
+ Logger_1.default.warn('MongoAdapter', `Invalid ObjectId for '${key}': ${v}`);
283
+ return v;
284
+ }
285
+ };
286
+ if (Array.isArray(val)) {
287
+ newQuery[key] = val.map(toObjectId);
288
+ }
289
+ else {
290
+ newQuery[key] = toObjectId(val);
291
+ }
292
+ }
293
+ }
294
+ return newQuery;
295
+ }
296
+ toAppObject(doc, includePrivateFields) {
297
+ if (!doc)
298
+ return doc;
299
+ const obj = doc.toObject(includePrivateFields
300
+ ? { transform: (_doc, ret) => ret } // override to keep private fields
301
+ : undefined);
302
+ if (obj?._id !== undefined) {
303
+ obj.id = String(obj._id);
304
+ delete obj._id;
305
+ }
306
+ if ('__v' in obj)
307
+ delete obj.__v;
308
+ return obj;
309
+ }
310
+ // ---------------- CRUD ----------------
311
+ async create(collection, data, schemaDefinition, includePrivateFields = false) {
312
+ const model = this.getModel(collection, schemaDefinition);
313
+ const doc = new model(data);
314
+ const saved = await doc.save();
315
+ return this.toAppObject(saved, includePrivateFields);
316
+ }
317
+ async read(collection, query, limit, skip, schemaDefinition, includePrivateFields, options) {
318
+ const model = this.getModel(collection, schemaDefinition);
319
+ const q = this.convertQueryFieldsToObjectId(query, schemaDefinition);
320
+ let cursor = model.find(q, options?.projection); // ✅ projection support (optional)
321
+ if (options?.sort && Object.keys(options.sort).length) {
322
+ cursor = cursor.sort(options.sort); // ✅ apply sort
323
+ }
324
+ if (limit !== undefined && skip !== undefined) {
325
+ cursor = cursor.limit(limit).skip(skip);
326
+ }
327
+ const results = await cursor.exec();
328
+ return results.map((doc) => this.toAppObject(doc, includePrivateFields));
329
+ }
330
+ async count(collection, query, schemaDefinition) {
331
+ const model = this.getModel(collection, schemaDefinition);
332
+ const q = this.convertQueryFieldsToObjectId(query, schemaDefinition);
333
+ return model.countDocuments(q).exec();
334
+ }
335
+ async update(collection, id, data, schemaDefinition, includePrivateFields) {
336
+ const model = this.getModel(collection, schemaDefinition);
337
+ const { _id } = this.convertQueryFieldsToObjectId({ id }, schemaDefinition);
338
+ const updated = await model
339
+ .findByIdAndUpdate(_id, data, { new: true, runValidators: true })
340
+ .exec();
341
+ if (!updated) {
342
+ throw new Error(`Document with id ${id} not found in collection '${collection}'`);
343
+ }
344
+ return this.toAppObject(updated, includePrivateFields);
345
+ }
346
+ async delete(collection, id, schemaDefinition, includePrivateFields = false) {
347
+ const model = this.getModel(collection, schemaDefinition);
348
+ const { _id } = this.convertQueryFieldsToObjectId({ id }, schemaDefinition);
349
+ const deleted = await model.findByIdAndDelete(_id).exec();
350
+ if (!deleted) {
351
+ throw new Error(`Document with id ${id} not found in collection '${collection}'`);
352
+ }
353
+ return this.toAppObject(deleted, includePrivateFields);
354
+ }
355
+ getNativeCollectionByModelName(modelName) {
356
+ const s = this.getSchema(modelName);
357
+ if (!s)
358
+ throw new Error(`Schema not found for ${modelName}`);
359
+ const collName = s.collection?.toLowerCase?.() ?? s.modelName.toLowerCase();
360
+ // returns the MongoDB driver's Collection (not a Mongoose Model)
361
+ return this.connection.collection(collName);
362
+ }
363
+ /** Create/drop single-field indexes based on schema plan.
364
+ * Only touches indexes named with the prefix below (won't touch user indexes).
365
+ */
366
+ managedIndexName(field) {
367
+ return `nextmin_idx_${field}`;
368
+ }
369
+ async syncIndexes(modelName, desired) {
370
+ const col = this.getNativeCollectionByModelName(modelName);
371
+ // list existing and pick only the ones we manage
372
+ const existing = await col.indexes();
373
+ const managed = new Map();
374
+ for (const ix of existing) {
375
+ const name = String(ix.name || '');
376
+ if (name.startsWith('nextmin_idx_')) {
377
+ managed.set(name, { key: ix.key });
378
+ }
379
+ }
380
+ // desired names from spec
381
+ const desiredEntries = Object.entries(desired);
382
+ const desiredNames = new Set(desiredEntries.map(([field]) => this.managedIndexName(field)));
383
+ // create / recreate if needed
384
+ for (const [field, dir] of desiredEntries) {
385
+ const name = this.managedIndexName(field);
386
+ const have = managed.get(name);
387
+ const wantKey = { [field]: dir };
388
+ if (!have || JSON.stringify(have.key) !== JSON.stringify(wantKey)) {
389
+ if (have) {
390
+ try {
391
+ await col.dropIndex(name);
392
+ }
393
+ catch { }
394
+ }
395
+ await col.createIndex(wantKey, { name, background: true });
396
+ }
397
+ }
398
+ // drop stale managed indexes
399
+ for (const name of managed.keys()) {
400
+ if (!desiredNames.has(name)) {
401
+ try {
402
+ await col.dropIndex(name);
403
+ }
404
+ catch { }
405
+ }
406
+ }
407
+ }
408
+ }
409
+ exports.MongoAdapter = MongoAdapter;
@@ -0,0 +1,35 @@
1
+ export type FileProvider = 's3' | 'gcs' | 'local' | (string & {});
2
+ export interface UploadPayload {
3
+ /** Path-like key inside bucket/provider, e.g. "uploads/userId/2025/08/18/file.png" */
4
+ key?: string;
5
+ /** Raw file content */
6
+ body: Buffer | Uint8Array | ArrayBuffer;
7
+ /** MIME type */
8
+ contentType?: string;
9
+ /** Arbitrary metadata */
10
+ metadata?: Record<string, string>;
11
+ }
12
+ export interface UploadResult {
13
+ provider: FileProvider;
14
+ /** Provider bucket/container if applicable */
15
+ bucket?: string;
16
+ /** Final key used for storage */
17
+ key: string;
18
+ /** Public (or CDN) URL if available; otherwise a standard provider URL */
19
+ url: string;
20
+ etag?: string;
21
+ contentType?: string;
22
+ size?: number;
23
+ metadata?: Record<string, string>;
24
+ }
25
+ /** Pluggable storage provider interface (S3 now; GCS/local later) */
26
+ export interface FileStorageAdapter {
27
+ readonly name: FileProvider;
28
+ upload(input: UploadPayload): Promise<UploadResult>;
29
+ delete(key: string): Promise<{
30
+ deleted: boolean;
31
+ }>;
32
+ /** Optional helpers */
33
+ getPublicUrl?(key: string): string | null;
34
+ getSignedUrl?(key: string, expiresInSeconds?: number): Promise<string>;
35
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,30 @@
1
+ import type { FileStorageAdapter, UploadPayload, UploadResult } from './FileStorageAdapter';
2
+ export interface S3AdapterOptions {
3
+ bucket: string;
4
+ region: string;
5
+ credentials?: {
6
+ accessKeyId: string;
7
+ secretAccessKey: string;
8
+ };
9
+ endpoint?: string;
10
+ forcePathStyle?: boolean;
11
+ defaultACL?: 'private' | 'public-read';
12
+ publicBaseUrl?: string;
13
+ }
14
+ export declare class S3FileStorageAdapter implements FileStorageAdapter {
15
+ readonly name: "s3";
16
+ private readonly s3;
17
+ private readonly bucket;
18
+ private readonly regionStr;
19
+ private readonly endpoint?;
20
+ private readonly forcePathStyle;
21
+ private readonly defaultACL?;
22
+ private readonly publicBaseUrl?;
23
+ constructor(opts: S3AdapterOptions);
24
+ upload(input: UploadPayload): Promise<UploadResult>;
25
+ delete(key: string): Promise<{
26
+ deleted: boolean;
27
+ }>;
28
+ getPublicUrl(key: string): string;
29
+ private randomKey;
30
+ }