@airoom/nextmin-node 2.0.2 → 2.0.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.
Files changed (37) hide show
  1. package/README.md +151 -0
  2. package/dist/api/apiRouter.d.ts +20 -0
  3. package/dist/api/apiRouter.js +122 -7
  4. package/dist/api/router/mountCrudRoutes.js +69 -18
  5. package/dist/api/router/setupAuthRoutes.js +475 -38
  6. package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
  7. package/dist/api/router/setupChatWidgetRoutes.js +207 -0
  8. package/dist/api/router/setupFileRoutes.js +221 -16
  9. package/dist/api/router/utils.d.ts +2 -1
  10. package/dist/api/router/utils.js +8 -6
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +145 -64
  13. package/dist/database/DatabaseAdapter.d.ts +1 -0
  14. package/dist/database/NMAdapter.d.ts +6 -0
  15. package/dist/database/NMAdapter.js +437 -86
  16. package/dist/database/QueryEngine.js +7 -4
  17. package/dist/files/FileStorageAdapter.d.ts +1 -0
  18. package/dist/files/LocalFileStorageAdapter.d.ts +1 -0
  19. package/dist/files/LocalFileStorageAdapter.js +25 -3
  20. package/dist/files/S3FileStorageAdapter.d.ts +3 -1
  21. package/dist/files/S3FileStorageAdapter.js +72 -11
  22. package/dist/files/filename.js +6 -4
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +3 -1
  25. package/dist/models/BaseModel.d.ts +4 -0
  26. package/dist/policy/authorize.d.ts +1 -1
  27. package/dist/policy/authorize.js +64 -13
  28. package/dist/schemas/Users.json +20 -10
  29. package/dist/services/IndexingService.d.ts +24 -0
  30. package/dist/services/IndexingService.js +555 -0
  31. package/dist/services/LocalAIService.d.ts +34 -0
  32. package/dist/services/LocalAIService.js +471 -0
  33. package/dist/services/OpenAIService.d.ts +16 -0
  34. package/dist/services/OpenAIService.js +586 -0
  35. package/dist/utils/SchemaLoader.d.ts +1 -1
  36. package/dist/utils/SchemaLoader.js +40 -4
  37. package/package.json +12 -4
@@ -126,22 +126,25 @@ class QueryEngine {
126
126
  }
127
127
  if (!attr)
128
128
  continue;
129
+ const a = Array.isArray(attr) ? attr[0] : attr;
130
+ const attrType = a && String(a.type || '').toLowerCase();
131
+ const isStringField = attrType === 'string' || attrType === 'text';
129
132
  const v = Array.isArray(vRaw) ? vRaw.map(String).join(',') : String(vRaw);
130
133
  if (!v?.length)
131
134
  continue;
132
135
  if (v.includes(',')) {
133
- filter[effectiveKey] = { $in: v.split(',').map(s => this.coerceValue(s.trim())) };
136
+ filter[effectiveKey] = { $in: v.split(',').map(s => isStringField ? s.trim() : this.coerceValue(s.trim())) };
134
137
  }
135
138
  else if (v.startsWith('!')) {
136
- filter[effectiveKey] = { $ne: this.coerceValue(v.slice(1)) };
139
+ filter[effectiveKey] = { $ne: isStringField ? v.slice(1) : this.coerceValue(v.slice(1)) };
137
140
  }
138
141
  else if (/^(gt|gte|lt|lte):/i.test(v)) {
139
142
  const [op, rest] = v.split(':', 2);
140
143
  const map = { gt: '$gt', gte: '$gte', lt: '$lt', lte: '$lte' };
141
- filter[effectiveKey] = { [map[op.toLowerCase()]]: this.coerceValue(rest) };
144
+ filter[effectiveKey] = { [map[op.toLowerCase()]]: isStringField ? rest : this.coerceValue(rest) };
142
145
  }
143
146
  else {
144
- filter[effectiveKey] = this.coerceValue(v);
147
+ filter[effectiveKey] = isStringField ? v : this.coerceValue(v);
145
148
  }
146
149
  }
147
150
  return filter;
@@ -36,6 +36,7 @@ export interface FileStorageAdapter {
36
36
  delete(key: string): Promise<{
37
37
  deleted: boolean;
38
38
  }>;
39
+ exists?(key: string): Promise<boolean>;
39
40
  /** Optional helpers */
40
41
  getPublicUrl?(key: string): string | null;
41
42
  getSignedUrl?(key: string, expiresInSeconds?: number): Promise<string>;
@@ -8,6 +8,7 @@ export declare class LocalFileStorageAdapter implements FileStorageAdapter {
8
8
  private uploadDir;
9
9
  private publicUrlPrefix;
10
10
  constructor(options: LocalFileStorageOptions);
11
+ exists(key: string): Promise<boolean>;
11
12
  upload(input: UploadPayload): Promise<UploadResult>;
12
13
  download(key: string): Promise<{
13
14
  body: any;
@@ -18,11 +18,33 @@ class LocalFileStorageAdapter {
18
18
  fs_1.default.mkdirSync(this.uploadDir, { recursive: true });
19
19
  }
20
20
  }
21
+ async exists(key) {
22
+ const filePath = path_1.default.join(this.uploadDir, key);
23
+ return fs_1.default.existsSync(filePath);
24
+ }
21
25
  async upload(input) {
22
- const originalName = input.originalFilename || 'unknown-file';
23
- const randomId = crypto_1.default.randomBytes(4).toString('hex');
24
- const uniqueName = input.key || (0, filename_1.filenameWithRandomId)(originalName, randomId);
26
+ let uniqueName = input.key;
27
+ if (!uniqueName) {
28
+ if (input.originalFilename) {
29
+ const lastDot = input.originalFilename.lastIndexOf('.');
30
+ const hasExt = lastDot > 0 && lastDot < input.originalFilename.length - 1;
31
+ const nameWithoutExt = hasExt ? input.originalFilename.slice(0, lastDot) : input.originalFilename;
32
+ const ext = hasExt ? input.originalFilename.slice(lastDot).toLowerCase() : '';
33
+ const randomId = crypto_1.default.randomBytes(4).toString('hex');
34
+ const cleanName = (0, filename_1.slugify)(nameWithoutExt) || randomId;
35
+ uniqueName = `${cleanName}${ext}`;
36
+ }
37
+ else {
38
+ uniqueName = crypto_1.default.randomBytes(8).toString('hex');
39
+ }
40
+ }
25
41
  const filePath = path_1.default.join(this.uploadDir, uniqueName);
42
+ if (fs_1.default.existsSync(filePath)) {
43
+ const err = new Error(`File '${uniqueName}' already exists in storage. Please rename the file manually and try again.`);
44
+ err.statusCode = 409;
45
+ err.code = 'FILE_EXISTS';
46
+ throw err;
47
+ }
26
48
  const writeStream = fs_1.default.createWriteStream(filePath);
27
49
  // Convert body to stream if it is a Buffer
28
50
  const readStream = new stream_1.Readable();
@@ -20,6 +20,7 @@ export declare class S3FileStorageAdapter implements FileStorageAdapter {
20
20
  private readonly forcePathStyle;
21
21
  private readonly defaultACL?;
22
22
  private readonly publicBaseUrl?;
23
+ private readonly credentials?;
23
24
  constructor(opts: S3AdapterOptions);
24
25
  upload(input: UploadPayload): Promise<UploadResult>;
25
26
  download(key: string): Promise<{
@@ -30,6 +31,7 @@ export declare class S3FileStorageAdapter implements FileStorageAdapter {
30
31
  delete(key: string): Promise<{
31
32
  deleted: boolean;
32
33
  }>;
34
+ exists(key: string): Promise<boolean>;
33
35
  getPublicUrl(key: string): string;
34
- private randomKey;
36
+ private buildKey;
35
37
  }
@@ -16,24 +16,49 @@ class S3FileStorageAdapter {
16
16
  this.forcePathStyle = opts.forcePathStyle ?? true; // sane default for MinIO
17
17
  this.defaultACL = opts.defaultACL;
18
18
  this.publicBaseUrl = opts.publicBaseUrl;
19
+ this.credentials = opts.credentials;
19
20
  this.s3 = new client_s3_1.S3Client({
20
21
  region: this.regionStr,
21
22
  endpoint: this.endpoint,
22
23
  forcePathStyle: this.forcePathStyle,
23
24
  credentials: opts.credentials,
25
+ requestChecksumCalculation: 'WHEN_REQUIRED',
24
26
  });
25
27
  }
26
28
  async upload(input) {
27
29
  if (!input.body)
28
30
  throw new Error('S3 upload: "body" is required');
29
- const key = input.key ?? this.randomKey(input.originalFilename);
31
+ const key = input.key ?? this.buildKey(input.originalFilename);
32
+ const alreadyExists = await this.exists(key);
33
+ if (alreadyExists) {
34
+ const filename = key.split('/').pop() || key;
35
+ const err = new Error(`File '${filename}' already exists in storage. Please rename the file manually and try again.`);
36
+ err.statusCode = 409;
37
+ err.code = 'FILE_EXISTS';
38
+ throw err;
39
+ }
40
+ const sanitizedMetadata = {};
41
+ if (input.metadata) {
42
+ for (const [k, v] of Object.entries(input.metadata)) {
43
+ if (typeof v === 'string') {
44
+ // Replace non-ascii spaces (e.g. narrow no-break space U+202F, no-break space U+00A0)
45
+ // and keep only printable ASCII characters (0x20 to 0x7E) to satisfy S3 metadata specs.
46
+ sanitizedMetadata[k] = v
47
+ .replace(/[\u200B-\u200D\uFEFF\u202F\u00A0]/g, ' ')
48
+ .replace(/[^\x20-\x7E]/g, '');
49
+ }
50
+ else {
51
+ sanitizedMetadata[k] = String(v);
52
+ }
53
+ }
54
+ }
30
55
  // Build params without ACL by default
31
56
  const params = {
32
57
  Bucket: this.bucket,
33
58
  Key: key,
34
59
  Body: input.body,
35
60
  ContentType: input.contentType,
36
- Metadata: input.metadata,
61
+ Metadata: sanitizedMetadata,
37
62
  CacheControl: input.contentType?.startsWith('image/')
38
63
  ? 'public, max-age=31536000, immutable'
39
64
  : 'public, max-age=86400',
@@ -43,7 +68,27 @@ class S3FileStorageAdapter {
43
68
  if (this.endpoint && this.defaultACL) {
44
69
  params.ACL = this.defaultACL;
45
70
  }
46
- await this.s3.send(new client_s3_1.PutObjectCommand(params));
71
+ try {
72
+ await this.s3.send(new client_s3_1.PutObjectCommand(params));
73
+ }
74
+ catch (err) {
75
+ console.error('[R2 UPLOAD ERROR] PutObject failed:', {
76
+ bucket: this.bucket,
77
+ key: key,
78
+ contentType: input.contentType,
79
+ endpoint: this.endpoint,
80
+ region: this.regionStr,
81
+ forcePathStyle: this.forcePathStyle,
82
+ defaultACL: this.defaultACL,
83
+ hasCredentials: !!this.credentials,
84
+ accessKeyIdLength: this.credentials?.accessKeyId?.length,
85
+ secretAccessKeyLength: this.credentials?.secretAccessKey?.length,
86
+ accessKeyIdStart: this.credentials?.accessKeyId?.slice(0, 4),
87
+ secretAccessKeyStart: this.credentials?.secretAccessKey?.slice(0, 4),
88
+ });
89
+ console.error('[R2 UPLOAD ERROR] Detailed Error:', err);
90
+ throw err;
91
+ }
47
92
  const url = this.getPublicUrl(key);
48
93
  return {
49
94
  provider: 's3',
@@ -73,6 +118,18 @@ class S3FileStorageAdapter {
73
118
  await this.s3.send(new client_s3_1.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
74
119
  return { deleted: true };
75
120
  }
121
+ async exists(key) {
122
+ try {
123
+ await this.s3.send(new client_s3_1.HeadObjectCommand({ Bucket: this.bucket, Key: key }));
124
+ return true;
125
+ }
126
+ catch (err) {
127
+ if (err.name === 'NotFound' || err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) {
128
+ return false;
129
+ }
130
+ return false;
131
+ }
132
+ }
76
133
  getPublicUrl(key) {
77
134
  const encKey = encodeKeySegments(key);
78
135
  if (this.publicBaseUrl) {
@@ -86,19 +143,23 @@ class S3FileStorageAdapter {
86
143
  return `https://${this.bucket}.s3.${this.regionStr}.amazonaws.com/${encKey}`;
87
144
  }
88
145
  // ---- private helpers ----
89
- randomKey(originalFilename) {
146
+ buildKey(originalFilename) {
90
147
  const ts = new Date();
91
148
  const y = ts.getUTCFullYear();
92
149
  const m = String(ts.getUTCMonth() + 1).padStart(2, '0');
93
150
  const d = String(ts.getUTCDate()).padStart(2, '0');
151
+ if (originalFilename) {
152
+ const lastDot = originalFilename.lastIndexOf('.');
153
+ const hasExt = lastDot > 0 && lastDot < originalFilename.length - 1;
154
+ const nameWithoutExt = hasExt ? originalFilename.slice(0, lastDot) : originalFilename;
155
+ const ext = hasExt ? originalFilename.slice(lastDot).toLowerCase() : '';
156
+ const randomId = Math.random().toString(36).slice(2, 10);
157
+ const cleanName = (0, filename_1.slugify)(nameWithoutExt) || randomId;
158
+ const filename = `${cleanName}${ext}`;
159
+ return `uploads/${y}/${m}/${d}/${filename}`;
160
+ }
94
161
  const randomId = Math.random().toString(36).slice(2, 10);
95
- // Generate filename: slugified-name-randomid.ext or just randomid if no original name
96
- const filename = originalFilename
97
- ? (0, filename_1.filenameWithRandomId)(originalFilename, randomId)
98
- : randomId;
99
- // Path: uploads/YYYY/MM/DD/filename
100
- const fullPath = `uploads/${y}/${m}/${d}/${filename}`;
101
- return fullPath;
162
+ return `uploads/${y}/${m}/${d}/${randomId}`;
102
163
  }
103
164
  }
104
165
  exports.S3FileStorageAdapter = S3FileStorageAdapter;
@@ -16,13 +16,15 @@ function sanitizeFilename(name) {
16
16
  * Example: "My Photo (2024).jpg" → "my-photo-2024.jpg"
17
17
  */
18
18
  function slugify(text) {
19
+ if (!text)
20
+ return '';
19
21
  return text
20
22
  .toLowerCase()
21
23
  .trim()
22
24
  // Replace spaces and underscores with hyphens
23
25
  .replace(/[\s_]+/g, '-')
24
- // Remove special characters except hyphens, dots, and alphanumeric
25
- .replace(/[^\w\-\.]+/g, '')
26
+ // Remove special characters except hyphens, dots, and alphanumeric/Unicode letters & marks
27
+ .replace(/[^\p{L}\p{N}\p{M}\-_.]+/gu, '')
26
28
  // Replace multiple consecutive hyphens with single hyphen
27
29
  .replace(/-+/g, '-')
28
30
  // Remove leading/trailing hyphens
@@ -40,8 +42,8 @@ function filenameWithRandomId(originalName, randomId) {
40
42
  const ext = hasExt ? originalName.slice(lastDot) : '';
41
43
  // Slugify the name part
42
44
  const slugifiedName = slugify(nameWithoutExt);
43
- // Combine: slugified-name-randomid.ext
44
- const result = `${slugifiedName}-${randomId}${ext}`;
45
+ // Combine: slugified-name-randomid.ext (or randomid.ext if empty)
46
+ const result = slugifiedName ? `${slugifiedName}-${randomId}${ext}` : `${randomId}${ext}`;
45
47
  return result;
46
48
  }
47
49
  function withTimestampPrefix(name) {
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './files/FileStorageAdapter';
8
8
  export * from './files/S3FileStorageAdapter';
9
9
  export * from './files/LocalFileStorageAdapter';
10
10
  export * from './files/filename';
11
+ export { reindex } from './services/IndexingService';
11
12
  /**
12
13
  * Compose the public router:
13
14
  * - JSON & urlencoded body parsers
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.generateApiKey = exports.Events = exports.events = exports.QueryEngine = exports.NMAdapter = void 0;
20
+ exports.reindex = exports.generateApiKey = exports.Events = exports.events = exports.QueryEngine = exports.NMAdapter = void 0;
21
21
  exports.createNextMinRouter = createNextMinRouter;
22
22
  const express_1 = __importDefault(require("express"));
23
23
  const body_parser_1 = __importDefault(require("body-parser"));
@@ -36,6 +36,8 @@ __exportStar(require("./files/FileStorageAdapter"), exports);
36
36
  __exportStar(require("./files/S3FileStorageAdapter"), exports);
37
37
  __exportStar(require("./files/LocalFileStorageAdapter"), exports);
38
38
  __exportStar(require("./files/filename"), exports);
39
+ var IndexingService_1 = require("./services/IndexingService");
40
+ Object.defineProperty(exports, "reindex", { enumerable: true, get: function () { return IndexingService_1.reindex; } });
39
41
  function isMultipart(req) {
40
42
  const ct = (req.headers['content-type'] || '').toLowerCase();
41
43
  return ct.startsWith('multipart/form-data');
@@ -53,6 +53,10 @@ export interface Schema {
53
53
  actions?: CustomAction[];
54
54
  access?: any;
55
55
  aggregates?: AggregateSpec[];
56
+ publicRegisterFields?: string[];
57
+ roleDefaultStatuses?: Record<string, string>;
58
+ authServices?: string[];
59
+ phoneVerification?: boolean;
56
60
  }
57
61
  export interface AggregateSpec {
58
62
  sourceModel: string;
@@ -21,5 +21,5 @@ export declare function applyReadMaskOne(doc: any, mask?: string[]): any;
21
21
  export declare function applyReadMaskMany(docs: any[], mask?: string[]): any[];
22
22
  export declare function stripWriteDeny(payload: any, deny?: string[]): any;
23
23
  export declare function mergeCreateDefaults(payload: any, defs?: Record<string, any>): any;
24
- export declare function enforceRestrictions(payload: any, restrictions?: Record<string, any>, _ctx?: PolicyContext): void;
24
+ export declare function enforceRestrictions(payload: any, restrictions?: Record<string, any>, _ctx?: PolicyContext, routerCtx?: any): Promise<void>;
25
25
  export declare function andFilter(a?: Record<string, any>, b?: Record<string, any>): Record<string, any>;
@@ -59,22 +59,51 @@ function asBool(v) {
59
59
  return typeof v === 'boolean' ? v : undefined;
60
60
  }
61
61
  function ownerKey(schemaPolicy) {
62
- const by = schemaPolicy?.conditions?.owner?.by;
62
+ const by = schemaPolicy?.conditions?.owner?.by || schemaPolicy?.access?.conditions?.owner?.by;
63
63
  return typeof by === 'string' && by.trim() ? by.trim() : 'id';
64
64
  }
65
- function ownerFilter(schemaPolicy, ctx) {
66
- const key = ownerKey(schemaPolicy);
65
+ function ownerFilter(schemaPolicy, ctx, modelNameLC) {
67
66
  if (!ctx.userId)
68
67
  return {};
68
+ const isUserModel = modelNameLC === 'users' || modelNameLC === 'user';
69
+ if (isUserModel) {
70
+ if (ctx.role === 'admin') {
71
+ return {
72
+ or: [
73
+ { id: ctx.userId },
74
+ { createdBy: ctx.userId }
75
+ ]
76
+ };
77
+ }
78
+ return { id: ctx.userId };
79
+ }
80
+ const key = ownerKey(schemaPolicy);
69
81
  return { [key]: ctx.userId };
70
82
  }
71
- function isOwnerDoc(schemaPolicy, ctx, doc) {
83
+ function isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC) {
72
84
  if (!doc || !ctx.userId)
73
85
  return false;
86
+ const isUserModel = modelNameLC === 'users' || modelNameLC === 'user';
87
+ if (isUserModel) {
88
+ const isSelf = String(doc.id ?? doc._id ?? '') === String(ctx.userId);
89
+ const isCreator = doc.createdBy && String(typeof doc.createdBy === 'object' ? (doc.createdBy.id ?? doc.createdBy._id ?? doc.createdBy) : doc.createdBy) === String(ctx.userId);
90
+ if (ctx.role === 'admin') {
91
+ return isSelf || isCreator;
92
+ }
93
+ return isSelf;
94
+ }
74
95
  const key = ownerKey(schemaPolicy);
75
96
  const val = doc[key];
76
97
  if (val == null)
77
98
  return false;
99
+ if (Array.isArray(val)) {
100
+ return val.some((v) => {
101
+ if (v == null)
102
+ return false;
103
+ const candidate = typeof v === 'object' ? (v.id ?? v._id ?? String(v)) : String(v);
104
+ return String(candidate) === String(ctx.userId);
105
+ });
106
+ }
78
107
  const candidate = typeof val === 'object' ? (val.id ?? val._id ?? String(val)) : String(val);
79
108
  return String(candidate) === String(ctx.userId);
80
109
  }
@@ -121,6 +150,20 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
121
150
  const allowed = !!schemaPolicy?.allowedMethods?.[action];
122
151
  if (!allowed)
123
152
  return { ...EMPTY_DECISION, allow: false };
153
+ // Bypass all access rules for superadmin and admin roles
154
+ if (ctx.isSuperadmin || ctx.role === 'superadmin' || ctx.role === 'admin') {
155
+ const sensitiveMask = computeSensitiveMask(schemaPolicy);
156
+ return {
157
+ allow: true,
158
+ readMask: sensitiveMask,
159
+ writeDeny: [],
160
+ createDefaults: {},
161
+ restrictions: {},
162
+ queryFilter: {},
163
+ exposePrivate: true,
164
+ sensitiveMask,
165
+ };
166
+ }
124
167
  const access = schemaPolicy?.access || {};
125
168
  const baseReadMask = [].concat(access?.readMask || []);
126
169
  // Auto-mask private attributes
@@ -228,7 +271,7 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
228
271
  if (action === 'read') {
229
272
  if (doc) {
230
273
  return finalize({
231
- allow: isOwnerDoc(schemaPolicy, ctx, doc),
274
+ allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
232
275
  readMask: effectiveReadMask,
233
276
  writeDeny: effectiveWriteDeny,
234
277
  createDefaults: {},
@@ -244,13 +287,13 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
244
287
  writeDeny: effectiveWriteDeny,
245
288
  createDefaults: {},
246
289
  restrictions: {},
247
- queryFilter: ownerFilter(schemaPolicy, ctx),
290
+ queryFilter: ownerFilter(schemaPolicy, ctx, modelNameLC),
248
291
  exposePrivate: bypass,
249
292
  sensitiveMask,
250
293
  });
251
294
  }
252
295
  return finalize({
253
- allow: isOwnerDoc(schemaPolicy, ctx, doc),
296
+ allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
254
297
  readMask: effectiveReadMask,
255
298
  writeDeny: effectiveWriteDeny,
256
299
  createDefaults: {},
@@ -289,7 +332,7 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
289
332
  if (action === 'read') {
290
333
  if (doc) {
291
334
  return finalize({
292
- allow: isOwnerDoc(schemaPolicy, ctx, doc),
335
+ allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
293
336
  readMask: effectiveReadMask,
294
337
  writeDeny: effectiveWriteDeny,
295
338
  createDefaults: {},
@@ -305,13 +348,13 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
305
348
  writeDeny: effectiveWriteDeny,
306
349
  createDefaults: {},
307
350
  restrictions: {},
308
- queryFilter: ownerFilter(schemaPolicy, ctx),
351
+ queryFilter: ownerFilter(schemaPolicy, ctx, modelNameLC),
309
352
  exposePrivate: bypass,
310
353
  sensitiveMask,
311
354
  });
312
355
  }
313
356
  return finalize({
314
- allow: isOwnerDoc(schemaPolicy, ctx, doc),
357
+ allow: isOwnerDoc(schemaPolicy, ctx, doc, modelNameLC),
315
358
  readMask: effectiveReadMask,
316
359
  writeDeny: effectiveReadMask, // (typo guard: will be ignored by callers; leave as is)
317
360
  createDefaults: {},
@@ -360,12 +403,20 @@ function mergeCreateDefaults(payload, defs) {
360
403
  return payload;
361
404
  return { ...defs, ...payload };
362
405
  }
363
- function enforceRestrictions(payload, restrictions, _ctx) {
406
+ async function enforceRestrictions(payload, restrictions, _ctx, routerCtx) {
364
407
  if (!payload || !restrictions)
365
408
  return;
366
409
  for (const [field, blocked] of Object.entries(restrictions)) {
367
- if (Array.isArray(blocked) && blocked.includes(payload[field])) {
368
- throw new Error(`Restricted value for field: ${field}`);
410
+ let val = payload[field];
411
+ if (val != null) {
412
+ if (field === 'role' && routerCtx?.normalizeRoleName) {
413
+ const resolvedRole = await routerCtx.normalizeRoleName(val);
414
+ if (resolvedRole)
415
+ val = resolvedRole;
416
+ }
417
+ if (Array.isArray(blocked) && blocked.map(String).includes(String(val))) {
418
+ throw new Error(`Restricted value for field: ${field}`);
419
+ }
369
420
  }
370
421
  }
371
422
  }
@@ -68,13 +68,18 @@
68
68
  "default": "user",
69
69
  "required": true,
70
70
  "safe": true
71
+ },
72
+ "createdBy": {
73
+ "type": "ObjectId",
74
+ "ref": "Users",
75
+ "private": true
71
76
  }
72
77
  },
73
78
  "allowedMethods": {
74
79
  "create": true,
75
80
  "read": true,
76
81
  "update": true,
77
- "delete": false
82
+ "delete": true
78
83
  },
79
84
  "access": {
80
85
  "public": {
@@ -87,14 +92,14 @@
87
92
  "create": false,
88
93
  "read": "owner",
89
94
  "update": "owner",
90
- "delete": false
95
+ "delete": "owner"
91
96
  },
92
97
  "roles": {
93
98
  "admin": {
94
99
  "create": true,
95
100
  "read": true,
96
- "update": true,
97
- "delete": false
101
+ "update": "owner",
102
+ "delete": "owner"
98
103
  },
99
104
  "superadmin": {
100
105
  "create": true,
@@ -141,17 +146,22 @@
141
146
  "status": "pending"
142
147
  },
143
148
  "roles.admin": {
144
- "status": "active"
149
+ "status": "active",
150
+ "createdBy": "$CTX.userId"
145
151
  },
146
152
  "roles.superadmin": {
147
- "status": "active"
153
+ "status": "active",
154
+ "createdBy": "$CTX.userId"
148
155
  }
149
156
  },
150
157
  "restrictions": {
151
- "roles.admin.cannotAssign": {
152
- "role": [
153
- "superadmin"
154
- ]
158
+ "roles": {
159
+ "admin": {
160
+ "role": [
161
+ "superadmin",
162
+ "admin"
163
+ ]
164
+ }
155
165
  }
156
166
  }
157
167
  }
@@ -0,0 +1,24 @@
1
+ import { ChatWidgetConfig } from '../api/apiRouter';
2
+ export declare class IndexingService {
3
+ private chatWidget;
4
+ private force;
5
+ private dbAdapter?;
6
+ private itemKeyToId;
7
+ constructor(chatWidget: ChatWidgetConfig, force?: boolean, dbAdapter?: any);
8
+ private getManifestPath;
9
+ private readManifest;
10
+ private writeManifest;
11
+ private getScrapedPagesDir;
12
+ private writeScrapedPageLocal;
13
+ private deleteScrapedPageLocal;
14
+ private fetchSitemapUrls;
15
+ crawlSite(siteUrl: string, startPaths?: string[], concurrency?: number): Promise<Map<string, string>>;
16
+ private extractInternalLinks;
17
+ private indexDatabaseSchemas;
18
+ runIndex(): Promise<void>;
19
+ }
20
+ export declare function reindex(config: {
21
+ chatWidget: ChatWidgetConfig;
22
+ force?: boolean;
23
+ dbAdapter?: any;
24
+ }): Promise<void>;