@nest-admin/nestjs 0.11.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,3247 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ AdapterError: () => AdapterError,
25
+ AdminModule: () => AdminModule,
26
+ ConstraintError: () => ConstraintError,
27
+ FieldNotFoundError: () => FieldNotFoundError,
28
+ ForbiddenError: () => ForbiddenError,
29
+ InvalidQueryError: () => InvalidQueryError,
30
+ ModelNotFoundError: () => ModelNotFoundError,
31
+ NestAdminError: () => NestAdminError,
32
+ RecordNotFoundError: () => RecordNotFoundError,
33
+ UnauthorizedError: () => UnauthorizedError,
34
+ ValidationError: () => ValidationError,
35
+ adminAccountOf: () => adminAccountOf,
36
+ builtInAuth: () => builtInAuth,
37
+ generateSessionSecret: () => generateSessionSecret,
38
+ hashAdminPassword: () => hashAdminPassword,
39
+ isNestAdminError: () => isNestAdminError,
40
+ unsafeAllowAllRequests: () => unsafeAllowAllRequests,
41
+ verifyAdminPassword: () => verifyAdminPassword
42
+ });
43
+ module.exports = __toCommonJS(src_exports);
44
+
45
+ // ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.1_64048c8b0f36b1e1c1865447e0ade244/node_modules/tsup/assets/cjs_shims.js
46
+ var getImportMetaUrl = /* @__PURE__ */ __name(() => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href, "getImportMetaUrl");
47
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
48
+
49
+ // src/module.ts
50
+ var import_common11 = require("@nestjs/common");
51
+ var import_core10 = require("@nestjs/core");
52
+
53
+ // ../core/dist/index.js
54
+ function summarise(account) {
55
+ return {
56
+ id: account.id,
57
+ email: account.email,
58
+ ...account.name !== void 0 ? {
59
+ name: account.name
60
+ } : {}
61
+ };
62
+ }
63
+ __name(summarise, "summarise");
64
+ var CREATED = [
65
+ "createdAt",
66
+ "created_at",
67
+ "created",
68
+ "createdOn",
69
+ "insertedAt",
70
+ "inserted_at"
71
+ ];
72
+ var NOT_CREATED = [
73
+ "updated",
74
+ "modified",
75
+ "deleted",
76
+ "archived",
77
+ "expires",
78
+ "expired"
79
+ ];
80
+ function isDate(field) {
81
+ return field.kind === "datetime" && !field.isList && !field.relation;
82
+ }
83
+ __name(isDate, "isDate");
84
+ function excluded(name) {
85
+ const lower = name.toLowerCase();
86
+ return NOT_CREATED.some((word) => lower.startsWith(word));
87
+ }
88
+ __name(excluded, "excluded");
89
+ function createdFieldFor(model) {
90
+ const dates = model.fields.filter(isDate);
91
+ for (const conventional of CREATED) {
92
+ const match = dates.find((field) => field.name.toLowerCase() === conventional.toLowerCase());
93
+ if (match) return match.name;
94
+ }
95
+ const generated = dates.filter((field) => field.isGenerated && !excluded(field.name));
96
+ return generated.length === 1 ? generated[0]?.name : void 0;
97
+ }
98
+ __name(createdFieldFor, "createdFieldFor");
99
+ var CONVENTIONAL = [
100
+ "name",
101
+ "title",
102
+ "label",
103
+ "displayName",
104
+ "username",
105
+ "email",
106
+ "slug"
107
+ ];
108
+ function isReadable(field) {
109
+ return field.kind === "string" && !field.isList && !field.relation && // A generated string is a cuid or a uuid: readable characters, no meaning.
110
+ !field.isGenerated;
111
+ }
112
+ __name(isReadable, "isReadable");
113
+ function displayFieldFor(model) {
114
+ if (model.displayField !== void 0) return model.displayField;
115
+ const readable2 = model.fields.filter(isReadable);
116
+ for (const candidate of CONVENTIONAL) {
117
+ const match = readable2.find((field) => field.name === candidate);
118
+ if (match) return match.name;
119
+ }
120
+ const unique = readable2.find((field) => field.isUnique && !field.isId);
121
+ if (unique) return unique.name;
122
+ const plain = readable2.find((field) => !field.isId);
123
+ if (plain) return plain.name;
124
+ return model.primaryKey[0] ?? model.fields[0]?.name ?? "id";
125
+ }
126
+ __name(displayFieldFor, "displayFieldFor");
127
+ function inverseRelationField(field, models) {
128
+ const relation = field.relation;
129
+ if (!relation?.name) return void 0;
130
+ const target = models.find((model) => model.name === relation.targetModel);
131
+ if (!target) return void 0;
132
+ return target.fields.find((candidate) => candidate.relation?.name === relation.name && candidate !== field);
133
+ }
134
+ __name(inverseRelationField, "inverseRelationField");
135
+ function relationShape(field, models) {
136
+ const relation = field.relation;
137
+ if (!relation) return void 0;
138
+ if (relation.cardinality === "one") return "to-one";
139
+ const inverse = inverseRelationField(field, models);
140
+ return inverse?.relation?.cardinality === "many" ? "many-to-many" : "one-to-many";
141
+ }
142
+ __name(relationShape, "relationShape");
143
+ function detachBlockedReason(field, models) {
144
+ if (relationShape(field, models) !== "one-to-many") return void 0;
145
+ const inverse = inverseRelationField(field, models);
146
+ if (!inverse?.isRequired) return void 0;
147
+ const target = field.relation?.targetModel ?? "the related model";
148
+ return `${target}.${inverse.name} is required, so a ${target} record cannot exist without one. Delete the record, or point it at something else, instead of detaching it.`;
149
+ }
150
+ __name(detachBlockedReason, "detachBlockedReason");
151
+ function selectModels(models, selection) {
152
+ if (!selection) return models;
153
+ const included = selection.include ? new Set(selection.include) : void 0;
154
+ const excluded2 = new Set(selection.exclude ?? []);
155
+ return models.filter((model) => (included === void 0 || included.has(model.name)) && !excluded2.has(model.name));
156
+ }
157
+ __name(selectModels, "selectModels");
158
+ function unknownSelectionNames(models, selection) {
159
+ if (!selection) return [];
160
+ const known = new Set(models.map((model) => model.name));
161
+ const referenced = [
162
+ ...selection.include ?? [],
163
+ ...selection.exclude ?? []
164
+ ];
165
+ return [
166
+ ...new Set(referenced.filter((name) => !known.has(name)))
167
+ ];
168
+ }
169
+ __name(unknownSelectionNames, "unknownSelectionNames");
170
+ function fieldOverride(overrides, model, field) {
171
+ return overrides?.[model]?.fields?.[field];
172
+ }
173
+ __name(fieldOverride, "fieldOverride");
174
+ function isReadOnly(overrides, model, field) {
175
+ return field.isGenerated || fieldOverride(overrides, model, field.name)?.readOnly === true;
176
+ }
177
+ __name(isReadOnly, "isReadOnly");
178
+ function applyOverrides(models, overrides) {
179
+ if (!overrides) return models;
180
+ return models.map((model) => {
181
+ const override = overrides[model.name];
182
+ if (!override) return model;
183
+ const hidden = new Set(Object.entries(override.fields ?? {}).filter(([, field]) => field.hidden === true).map(([name]) => name));
184
+ const writeOnly = new Set(Object.entries(override.fields ?? {}).filter(([, field]) => field.writeOnly === true).map(([name]) => name));
185
+ const kept = hidden.size === 0 ? model.fields : model.fields.filter((f) => !hidden.has(f.name));
186
+ return {
187
+ ...model,
188
+ ...override.displayField !== void 0 ? {
189
+ displayField: override.displayField
190
+ } : {},
191
+ // Carried onto the metadata rather than looked up again later, so
192
+ // everything downstream - the field scope, the projection, the DTO -
193
+ // reads one flag instead of each re-deriving it from the configuration.
194
+ fields: writeOnly.size === 0 ? kept : kept.map((field) => writeOnly.has(field.name) ? {
195
+ ...field,
196
+ writeOnly: true
197
+ } : field)
198
+ };
199
+ });
200
+ }
201
+ __name(applyOverrides, "applyOverrides");
202
+ function unknownOverrideNames(models, overrides) {
203
+ if (!overrides) return [];
204
+ const unknown = [];
205
+ for (const [modelName, override] of Object.entries(overrides)) {
206
+ const model = models.find((candidate) => candidate.name === modelName);
207
+ if (!model) {
208
+ unknown.push(modelName);
209
+ continue;
210
+ }
211
+ const names = new Set(model.fields.map((field) => field.name));
212
+ if (override.displayField !== void 0 && !names.has(override.displayField)) {
213
+ unknown.push(`${modelName}.${override.displayField}`);
214
+ }
215
+ for (const fieldName of Object.keys(override.fields ?? {})) {
216
+ if (!names.has(fieldName)) unknown.push(`${modelName}.${fieldName}`);
217
+ }
218
+ }
219
+ return unknown;
220
+ }
221
+ __name(unknownOverrideNames, "unknownOverrideNames");
222
+ function unwritableHiddenFields(models, overrides) {
223
+ if (!overrides) return [];
224
+ const blocked = [];
225
+ for (const [modelName, override] of Object.entries(overrides)) {
226
+ const model = models.find((candidate) => candidate.name === modelName);
227
+ if (!model) continue;
228
+ for (const [fieldName, field] of Object.entries(override.fields ?? {})) {
229
+ if (field.hidden !== true) continue;
230
+ const declared = model.fields.find((candidate) => candidate.name === fieldName);
231
+ if (!declared) continue;
232
+ if (declared.isRequired && !declared.isGenerated && declared.defaultValue === void 0) {
233
+ blocked.push(`${modelName}.${fieldName}`);
234
+ }
235
+ }
236
+ }
237
+ return blocked;
238
+ }
239
+ __name(unwritableHiddenFields, "unwritableHiddenFields");
240
+ var BRAND = /* @__PURE__ */ Symbol.for("nest-admin.error");
241
+ var NestAdminError = class extends Error {
242
+ static {
243
+ __name(this, "NestAdminError");
244
+ }
245
+ /**
246
+ * Which error this is.
247
+ *
248
+ * Subclasses override it with a literal. The base value covers anything that
249
+ * extends this class without declaring one - the Prisma schema errors, for
250
+ * instance - which the transport layer treats as internal.
251
+ */
252
+ kind = "unknown";
253
+ constructor(message, options) {
254
+ super(message, options);
255
+ this.name = new.target.name;
256
+ Object.defineProperty(this, BRAND, {
257
+ value: true,
258
+ enumerable: false
259
+ });
260
+ }
261
+ };
262
+ function isNestAdminError(value) {
263
+ return typeof value === "object" && value !== null && value[BRAND] === true;
264
+ }
265
+ __name(isNestAdminError, "isNestAdminError");
266
+ var ModelNotFoundError = class extends NestAdminError {
267
+ static {
268
+ __name(this, "ModelNotFoundError");
269
+ }
270
+ constructor(model, availableModels = []) {
271
+ const known = availableModels.length > 0 ? ` Known models: ${availableModels.join(", ")}.` : "";
272
+ super(`Unknown model "${model}".${known}`);
273
+ this.model = model;
274
+ this.availableModels = availableModels;
275
+ }
276
+ model;
277
+ availableModels;
278
+ kind = "model-not-found";
279
+ };
280
+ var FieldNotFoundError = class extends NestAdminError {
281
+ static {
282
+ __name(this, "FieldNotFoundError");
283
+ }
284
+ constructor(model, field, reason) {
285
+ super(`Unknown field "${field}" on model "${model}".${reason ? ` ${reason}` : ""}`);
286
+ this.model = model;
287
+ this.field = field;
288
+ }
289
+ model;
290
+ field;
291
+ kind = "field-not-found";
292
+ };
293
+ var RecordNotFoundError = class extends NestAdminError {
294
+ static {
295
+ __name(this, "RecordNotFoundError");
296
+ }
297
+ constructor(model, id) {
298
+ super(`No ${model} record found for id ${JSON.stringify(id)}.`);
299
+ this.model = model;
300
+ this.id = id;
301
+ }
302
+ model;
303
+ id;
304
+ kind = "record-not-found";
305
+ };
306
+ var InvalidQueryError = class extends NestAdminError {
307
+ static {
308
+ __name(this, "InvalidQueryError");
309
+ }
310
+ kind = "invalid-query";
311
+ };
312
+ var ValidationError = class extends NestAdminError {
313
+ static {
314
+ __name(this, "ValidationError");
315
+ }
316
+ constructor(message, fields = [], options) {
317
+ super(message, options);
318
+ this.fields = fields;
319
+ }
320
+ fields;
321
+ kind = "validation";
322
+ };
323
+ var ConstraintError = class extends NestAdminError {
324
+ static {
325
+ __name(this, "ConstraintError");
326
+ }
327
+ constructor(constraint, model, fields = []) {
328
+ super(describeConstraint(constraint, model, fields));
329
+ this.constraint = constraint;
330
+ this.model = model;
331
+ this.fields = fields;
332
+ }
333
+ constraint;
334
+ model;
335
+ fields;
336
+ kind = "constraint";
337
+ };
338
+ function describeConstraint(constraint, model, fields) {
339
+ const named = fields.length > 0 ? fields.join(", ") : void 0;
340
+ switch (constraint) {
341
+ case "unique":
342
+ return named ? `Another ${model} already has this ${named}.` : `Another ${model} already has one of these values.`;
343
+ case "foreign-key":
344
+ return named ? `The ${named} does not refer to an existing record, or the record it refers to is still in use.` : `A reference on this ${model} does not point at an existing record, or is still in use.`;
345
+ case "required":
346
+ return named ? `${named} is required.` : `A required value on this ${model} is missing.`;
347
+ }
348
+ }
349
+ __name(describeConstraint, "describeConstraint");
350
+ var AdapterError = class extends NestAdminError {
351
+ static {
352
+ __name(this, "AdapterError");
353
+ }
354
+ kind = "adapter";
355
+ constructor(message, options) {
356
+ super(message, options);
357
+ }
358
+ };
359
+ var UnauthorizedError = class extends NestAdminError {
360
+ static {
361
+ __name(this, "UnauthorizedError");
362
+ }
363
+ kind = "unauthorized";
364
+ constructor(message = "Authentication is required to access the admin API.") {
365
+ super(message);
366
+ }
367
+ };
368
+ var ForbiddenError = class extends NestAdminError {
369
+ static {
370
+ __name(this, "ForbiddenError");
371
+ }
372
+ kind = "forbidden";
373
+ constructor(message = "You do not have permission to access the admin API.") {
374
+ super(message);
375
+ }
376
+ };
377
+
378
+ // src/admin/controller.ts
379
+ var import_common7 = require("@nestjs/common");
380
+
381
+ // src/auth/guard.ts
382
+ var import_common = require("@nestjs/common");
383
+
384
+ // src/tokens.ts
385
+ var ADMIN_ADAPTER = /* @__PURE__ */ Symbol("NEST_ADMIN_ADAPTER");
386
+ var ADMIN_AUTH = /* @__PURE__ */ Symbol("NEST_ADMIN_AUTH");
387
+ var ADMIN_RESOURCE_AUTH = /* @__PURE__ */ Symbol("NEST_ADMIN_RESOURCE_AUTH");
388
+ var ADMIN_UI_ROOT = /* @__PURE__ */ Symbol("NEST_ADMIN_UI_ROOT");
389
+ var ADMIN_MOUNT_PATH = /* @__PURE__ */ Symbol("NEST_ADMIN_MOUNT_PATH");
390
+ var ADMIN_RESOURCES = /* @__PURE__ */ Symbol("NEST_ADMIN_RESOURCES");
391
+ var ADMIN_OPTIONS = /* @__PURE__ */ Symbol("NEST_ADMIN_OPTIONS");
392
+ var ADMIN_MODELS = /* @__PURE__ */ Symbol("NEST_ADMIN_MODELS");
393
+ var ADMIN_HOOKS = /* @__PURE__ */ Symbol("NEST_ADMIN_HOOKS");
394
+ var ADMIN_ACTIONS = /* @__PURE__ */ Symbol("NEST_ADMIN_ACTIONS");
395
+ var ADMIN_DASHBOARD = /* @__PURE__ */ Symbol("NEST_ADMIN_DASHBOARD");
396
+ var ADMIN_THEME = /* @__PURE__ */ Symbol("NEST_ADMIN_THEME");
397
+
398
+ // src/auth/guard.ts
399
+ function _ts_decorate(decorators, target, key, desc) {
400
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
401
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
402
+ r = Reflect.decorate(decorators, target, key, desc);
403
+ } else {
404
+ for (var i = decorators.length - 1; i >= 0; i--) {
405
+ if (d = decorators[i]) {
406
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
407
+ }
408
+ }
409
+ }
410
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
411
+ }
412
+ __name(_ts_decorate, "_ts_decorate");
413
+ function _ts_metadata(metadataKey, metadataValue) {
414
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
415
+ return Reflect.metadata(metadataKey, metadataValue);
416
+ }
417
+ }
418
+ __name(_ts_metadata, "_ts_metadata");
419
+ function _ts_param(paramIndex, decorator) {
420
+ return function(target, key) {
421
+ decorator(target, key, paramIndex);
422
+ };
423
+ }
424
+ __name(_ts_param, "_ts_param");
425
+ var AdminAuthGuard = class {
426
+ static {
427
+ __name(this, "AdminAuthGuard");
428
+ }
429
+ auth;
430
+ constructor(auth) {
431
+ this.auth = auth;
432
+ }
433
+ async canActivate(context) {
434
+ const decision = await this.auth.authorize(context);
435
+ if (decision === false) {
436
+ throw new ForbiddenError();
437
+ }
438
+ return true;
439
+ }
440
+ };
441
+ AdminAuthGuard = _ts_decorate([
442
+ (0, import_common.Injectable)(),
443
+ _ts_param(0, (0, import_common.Inject)(ADMIN_AUTH)),
444
+ _ts_metadata("design:type", Function),
445
+ _ts_metadata("design:paramtypes", [
446
+ typeof AdminAuth === "undefined" ? Object : AdminAuth
447
+ ])
448
+ ], AdminAuthGuard);
449
+
450
+ // src/http/execution-context.ts
451
+ var import_common2 = require("@nestjs/common");
452
+ var AdminContext = (0, import_common2.createParamDecorator)((_data, context) => context);
453
+
454
+ // src/http/exception.filter.ts
455
+ var import_common3 = require("@nestjs/common");
456
+
457
+ // src/http/response.ts
458
+ function success(data) {
459
+ return {
460
+ success: true,
461
+ data
462
+ };
463
+ }
464
+ __name(success, "success");
465
+ function successPage(data, meta) {
466
+ return {
467
+ success: true,
468
+ data,
469
+ meta
470
+ };
471
+ }
472
+ __name(successPage, "successPage");
473
+ function failure(code, message, details) {
474
+ return {
475
+ success: false,
476
+ error: {
477
+ code,
478
+ message,
479
+ ...details ? {
480
+ details
481
+ } : {}
482
+ }
483
+ };
484
+ }
485
+ __name(failure, "failure");
486
+
487
+ // src/http/exception.filter.ts
488
+ function _ts_decorate2(decorators, target, key, desc) {
489
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
490
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
491
+ r = Reflect.decorate(decorators, target, key, desc);
492
+ } else {
493
+ for (var i = decorators.length - 1; i >= 0; i--) {
494
+ if (d = decorators[i]) {
495
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
496
+ }
497
+ }
498
+ }
499
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
500
+ }
501
+ __name(_ts_decorate2, "_ts_decorate");
502
+ var INTERNAL = {
503
+ status: import_common3.HttpStatus.INTERNAL_SERVER_ERROR,
504
+ code: "INTERNAL_ERROR",
505
+ message: "An internal error occurred while handling the request."
506
+ };
507
+ function mapError(error) {
508
+ if (!isNestAdminError(error)) return INTERNAL;
509
+ switch (error.kind) {
510
+ // Auth first. No `details` on either: echoing anything about why a request
511
+ // was refused hands a prober information it did not have.
512
+ case "unauthorized":
513
+ return {
514
+ status: import_common3.HttpStatus.UNAUTHORIZED,
515
+ code: "UNAUTHORIZED",
516
+ message: error.message
517
+ };
518
+ case "forbidden":
519
+ return {
520
+ status: import_common3.HttpStatus.FORBIDDEN,
521
+ code: "FORBIDDEN",
522
+ message: error.message
523
+ };
524
+ case "model-not-found":
525
+ return {
526
+ status: import_common3.HttpStatus.NOT_FOUND,
527
+ code: "MODEL_NOT_FOUND",
528
+ message: error.message,
529
+ details: {
530
+ model: error.model
531
+ }
532
+ };
533
+ case "record-not-found":
534
+ return {
535
+ status: import_common3.HttpStatus.NOT_FOUND,
536
+ code: "RECORD_NOT_FOUND",
537
+ message: error.message,
538
+ details: {
539
+ model: error.model,
540
+ id: error.id
541
+ }
542
+ };
543
+ case "field-not-found":
544
+ return {
545
+ status: import_common3.HttpStatus.BAD_REQUEST,
546
+ code: "FIELD_NOT_FOUND",
547
+ message: error.message,
548
+ details: {
549
+ model: error.model,
550
+ field: error.field
551
+ }
552
+ };
553
+ // Raised by application code to refuse an input. The message is
554
+ // forwarded, which is what it is for.
555
+ case "validation": {
556
+ const refusal = error;
557
+ return {
558
+ status: import_common3.HttpStatus.BAD_REQUEST,
559
+ code: "VALIDATION_ERROR",
560
+ message: error.message,
561
+ // Only when it named them. An empty list would tell a client the
562
+ // refusal is about no field in particular, which is not the same as
563
+ // not saying - and the interface treats the two differently.
564
+ ...refusal.fields.length > 0 ? {
565
+ details: {
566
+ fields: refusal.fields
567
+ }
568
+ } : {}
569
+ };
570
+ }
571
+ // The database refused the write for a reason the caller can act on. The
572
+ // message is built from field names rather than taken from the ORM, so it
573
+ // carries no paths or query fragments and is safe to forward.
574
+ case "constraint": {
575
+ const failure2 = error;
576
+ return {
577
+ // A unique clash or a reference still in use is a conflict with data
578
+ // that already exists; a missing required value is a bad request.
579
+ status: failure2.constraint === "required" ? import_common3.HttpStatus.BAD_REQUEST : import_common3.HttpStatus.CONFLICT,
580
+ code: "CONSTRAINT_VIOLATION",
581
+ message: error.message,
582
+ details: {
583
+ constraint: failure2.constraint,
584
+ fields: failure2.fields
585
+ }
586
+ };
587
+ }
588
+ case "invalid-query":
589
+ return {
590
+ status: import_common3.HttpStatus.BAD_REQUEST,
591
+ code: "INVALID_QUERY",
592
+ message: error.message
593
+ };
594
+ // 'adapter', 'unknown', and any kind added later without a mapping. The
595
+ // default stays generic so a new internal error cannot start leaking.
596
+ default:
597
+ return INTERNAL;
598
+ }
599
+ }
600
+ __name(mapError, "mapError");
601
+ function clientMessage(error) {
602
+ return mapError(error).message;
603
+ }
604
+ __name(clientMessage, "clientMessage");
605
+ var AdminExceptionFilter = class {
606
+ static {
607
+ __name(this, "AdminExceptionFilter");
608
+ }
609
+ logger = new import_common3.Logger("NestAdmin");
610
+ catch(exception, host) {
611
+ if (exception instanceof import_common3.HttpException) {
612
+ throw exception;
613
+ }
614
+ const mapped = mapError(exception);
615
+ if (mapped.status >= import_common3.HttpStatus.INTERNAL_SERVER_ERROR) {
616
+ this.logger.error(isNestAdminError(exception) && exception.kind === "adapter" ? `Adapter failure: ${exception.message}` : "Unhandled error while handling an admin request", exception instanceof Error ? exception.stack : String(exception));
617
+ }
618
+ const body = failure(mapped.code, mapped.message, mapped.details);
619
+ const response = host.switchToHttp().getResponse();
620
+ response.status(mapped.status).json(body);
621
+ }
622
+ };
623
+ AdminExceptionFilter = _ts_decorate2([
624
+ (0, import_common3.Catch)()
625
+ ], AdminExceptionFilter);
626
+
627
+ // src/admin/service.ts
628
+ var import_common6 = require("@nestjs/common");
629
+
630
+ // src/auth/built-in.ts
631
+ var import_common4 = require("@nestjs/common");
632
+
633
+ // src/auth/password.ts
634
+ var import_node_crypto = require("crypto");
635
+ var import_node_util = require("util");
636
+ var derive = (0, import_node_util.promisify)(import_node_crypto.scrypt);
637
+ var PARAMS = {
638
+ N: 2 ** 15,
639
+ r: 8,
640
+ p: 1
641
+ };
642
+ var MAXMEM = 128 * PARAMS.N * PARAMS.r * 2;
643
+ var KEY_LENGTH = 64;
644
+ var SALT_BYTES = 16;
645
+ var SEP = String.fromCharCode(36);
646
+ async function hashAdminPassword(password) {
647
+ if (typeof password !== "string" || password.length === 0) {
648
+ throw new Error("A password is required.");
649
+ }
650
+ const salt = (0, import_node_crypto.randomBytes)(SALT_BYTES).toString("hex");
651
+ const key = await derive(password, salt, KEY_LENGTH, {
652
+ ...PARAMS,
653
+ maxmem: MAXMEM
654
+ });
655
+ return [
656
+ "scrypt",
657
+ PARAMS.N,
658
+ PARAMS.r,
659
+ PARAMS.p,
660
+ salt,
661
+ key.toString("hex")
662
+ ].join(SEP);
663
+ }
664
+ __name(hashAdminPassword, "hashAdminPassword");
665
+ async function verifyAdminPassword(password, stored) {
666
+ const parsed = parse(stored);
667
+ if (!parsed) return false;
668
+ try {
669
+ const key = await derive(password, parsed.salt, parsed.hash.length / 2, {
670
+ N: parsed.N,
671
+ r: parsed.r,
672
+ p: parsed.p,
673
+ maxmem: 128 * parsed.N * parsed.r * 2
674
+ });
675
+ const expected = Buffer.from(parsed.hash, "hex");
676
+ return key.length === expected.length && (0, import_node_crypto.timingSafeEqual)(key, expected);
677
+ } catch {
678
+ return false;
679
+ }
680
+ }
681
+ __name(verifyAdminPassword, "verifyAdminPassword");
682
+ function parse(stored) {
683
+ if (typeof stored !== "string") return void 0;
684
+ const [scheme, n, r, p, salt, hash] = stored.split(SEP);
685
+ if (scheme !== "scrypt" || salt === void 0 || hash === void 0) return void 0;
686
+ const N = Number(n);
687
+ const rounds = Number(r);
688
+ const parallel = Number(p);
689
+ const usable = Number.isInteger(N) && N >= 2 ** 12 && N <= 2 ** 20 && (N & N - 1) === 0 && Number.isInteger(rounds) && rounds > 0 && rounds <= 32 && Number.isInteger(parallel) && parallel > 0 && parallel <= 16 && /^[0-9a-f]+$/i.test(salt) && /^[0-9a-f]+$/i.test(hash) && hash.length % 2 === 0;
690
+ return usable ? {
691
+ N,
692
+ r: rounds,
693
+ p: parallel,
694
+ salt,
695
+ hash
696
+ } : void 0;
697
+ }
698
+ __name(parse, "parse");
699
+ var NO_SUCH_ACCOUNT = hashAdminPassword((0, import_node_crypto.randomBytes)(32).toString("hex"));
700
+
701
+ // src/auth/session.ts
702
+ var import_node_crypto2 = require("crypto");
703
+ var VERSION = "v1";
704
+ var encode = /* @__PURE__ */ __name((value) => Buffer.from(value).toString("base64url"), "encode");
705
+ function signSession(accountId, secret, lifetime) {
706
+ const payload = {
707
+ sub: accountId,
708
+ exp: Math.floor(Date.now() / 1e3) + lifetime
709
+ };
710
+ const body = `${VERSION}.${encode(JSON.stringify(payload))}`;
711
+ return `${body}.${sign(body, secret)}`;
712
+ }
713
+ __name(signSession, "signSession");
714
+ function readSession(token, secret) {
715
+ if (typeof token !== "string") return void 0;
716
+ const cut = token.lastIndexOf(".");
717
+ if (cut < 1) return void 0;
718
+ const body = token.slice(0, cut);
719
+ const presented = token.slice(cut + 1);
720
+ if (!body.startsWith(`${VERSION}.`)) return void 0;
721
+ if (!matches(presented, sign(body, secret))) return void 0;
722
+ try {
723
+ const payload = JSON.parse(Buffer.from(body.slice(VERSION.length + 1), "base64url").toString("utf8"));
724
+ if (typeof payload.sub !== "string" || payload.sub === "") return void 0;
725
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now()) return void 0;
726
+ return payload.sub;
727
+ } catch {
728
+ return void 0;
729
+ }
730
+ }
731
+ __name(readSession, "readSession");
732
+ function shouldRenew(token, secret, lifetime) {
733
+ const cut = token.lastIndexOf(".");
734
+ if (cut < 1 || !matches(token.slice(cut + 1), sign(token.slice(0, cut), secret))) return false;
735
+ try {
736
+ const payload = JSON.parse(Buffer.from(token.slice(VERSION.length + 1, cut), "base64url").toString("utf8"));
737
+ const remaining = payload.exp - Math.floor(Date.now() / 1e3);
738
+ return remaining < lifetime / 2;
739
+ } catch {
740
+ return false;
741
+ }
742
+ }
743
+ __name(shouldRenew, "shouldRenew");
744
+ function sign(body, secret) {
745
+ return (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("base64url");
746
+ }
747
+ __name(sign, "sign");
748
+ function matches(presented, expected) {
749
+ const a = Buffer.from(presented);
750
+ const b = Buffer.from(expected);
751
+ return a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
752
+ }
753
+ __name(matches, "matches");
754
+ var MIN_SECRET_LENGTH = 32;
755
+ function generateSessionSecret() {
756
+ return (0, import_node_crypto2.randomBytes)(32).toString("base64url");
757
+ }
758
+ __name(generateSessionSecret, "generateSessionSecret");
759
+
760
+ // src/auth/built-in.ts
761
+ var logger = new import_common4.Logger("NestAdmin");
762
+ var DEFAULT_MAX_AGE = 12 * 60 * 60;
763
+ var RUNTIME = /* @__PURE__ */ Symbol.for("nest-admin.built-in-auth");
764
+ function builtInRuntimeOf(auth) {
765
+ return typeof auth === "object" && auth !== null ? auth[RUNTIME] ?? void 0 : void 0;
766
+ }
767
+ __name(builtInRuntimeOf, "builtInRuntimeOf");
768
+ function adminAccountOf(context) {
769
+ const request = context.switchToHttp().getRequest();
770
+ return request?.adminAccount;
771
+ }
772
+ __name(adminAccountOf, "adminAccountOf");
773
+ function builtInAuth(options) {
774
+ const secret = options.session?.secret;
775
+ if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
776
+ throw new Error(`builtInAuth() requires \`session.secret\` of at least ${MIN_SECRET_LENGTH} characters. A short secret can be guessed, and a guessed one mints a session for any account. Read it from the environment rather than writing it here.`);
777
+ }
778
+ if (!options.store || typeof options.store.findByEmail !== "function") {
779
+ throw new Error("builtInAuth() requires a `store`. Use `prismaAccountStore({ client })` from `@nest-admin/nestjs/prisma`, or supply your own AdminAccountStore.");
780
+ }
781
+ const maxAge = options.session.maxAge ?? DEFAULT_MAX_AGE;
782
+ const cookieName = options.session.cookieName ?? "nest_admin_session";
783
+ const attempts = new Attempts(options.maxAttempts ?? 10, options.lockoutSeconds ?? 15 * 60);
784
+ const runtime = {
785
+ store: options.store,
786
+ secret,
787
+ maxAge,
788
+ cookieName,
789
+ secure: options.session.secure,
790
+ async signIn(email, password, from) {
791
+ if (typeof email !== "string" || typeof password !== "string") return void 0;
792
+ if (attempts.lockedOut(from)) return void 0;
793
+ const account = await options.store.findByEmail(email.trim().toLowerCase());
794
+ const stored = account?.passwordHash ?? await NO_SUCH_ACCOUNT;
795
+ const correct = await verifyAdminPassword(password, stored);
796
+ if (!correct || !account || account.disabled === true) {
797
+ attempts.failed(from);
798
+ return void 0;
799
+ }
800
+ attempts.succeeded(from);
801
+ void options.store.recordLogin?.(account.id).catch((cause) => {
802
+ logger.warn(`Could not record a login: ${String(cause)}`);
803
+ });
804
+ return account;
805
+ }
806
+ };
807
+ const auth = {
808
+ async authorize(context) {
809
+ const request = context.switchToHttp().getRequest();
810
+ const token = cookieFrom(request?.headers?.cookie, cookieName);
811
+ const id = token === void 0 ? void 0 : readSession(token, secret);
812
+ if (id === void 0) throw new UnauthorizedError("Sign in to continue.");
813
+ const account = await options.store.findById(id);
814
+ if (!account || account.disabled === true) {
815
+ throw new UnauthorizedError("Sign in to continue.");
816
+ }
817
+ request.adminAccount = summarise(account);
818
+ if (token !== void 0 && shouldRenew(token, secret, maxAge)) {
819
+ setSessionCookie(context.switchToHttp().getResponse(), signSession(account.id, secret, maxAge), runtime, request);
820
+ }
821
+ }
822
+ };
823
+ Object.defineProperty(auth, RUNTIME, {
824
+ value: runtime,
825
+ enumerable: false
826
+ });
827
+ return auth;
828
+ }
829
+ __name(builtInAuth, "builtInAuth");
830
+ function cookieFrom(header2, name) {
831
+ if (typeof header2 !== "string") return void 0;
832
+ for (const part of header2.split(";")) {
833
+ const eq = part.indexOf("=");
834
+ if (eq < 0) continue;
835
+ if (part.slice(0, eq).trim() !== name) continue;
836
+ const value = part.slice(eq + 1).trim();
837
+ try {
838
+ return decodeURIComponent(value);
839
+ } catch {
840
+ return value;
841
+ }
842
+ }
843
+ return void 0;
844
+ }
845
+ __name(cookieFrom, "cookieFrom");
846
+ function setSessionCookie(response, token, runtime, request) {
847
+ writeCookie(response, [
848
+ `${runtime.cookieName}=${token}`,
849
+ "Path=/",
850
+ "HttpOnly",
851
+ "SameSite=Lax",
852
+ `Max-Age=${runtime.maxAge}`,
853
+ ...isSecure(runtime.secure, request) ? [
854
+ "Secure"
855
+ ] : []
856
+ ].join("; "));
857
+ }
858
+ __name(setSessionCookie, "setSessionCookie");
859
+ function clearSessionCookie(response, runtime, request) {
860
+ writeCookie(response, [
861
+ `${runtime.cookieName}=`,
862
+ "Path=/",
863
+ "HttpOnly",
864
+ "SameSite=Lax",
865
+ "Max-Age=0",
866
+ ...isSecure(runtime.secure, request) ? [
867
+ "Secure"
868
+ ] : []
869
+ ].join("; "));
870
+ }
871
+ __name(clearSessionCookie, "clearSessionCookie");
872
+ function isSecure(configured, request) {
873
+ if (configured !== void 0) return configured;
874
+ const host = String(request?.headers?.["host"] ?? "");
875
+ const local = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(host);
876
+ return !local;
877
+ }
878
+ __name(isSecure, "isSecure");
879
+ function writeCookie(response, value) {
880
+ const target = response;
881
+ if (typeof target?.setHeader === "function") return target.setHeader("Set-Cookie", value);
882
+ if (typeof target?.header === "function") target.header("Set-Cookie", value);
883
+ }
884
+ __name(writeCookie, "writeCookie");
885
+ function attemptKey(request, email) {
886
+ const address = (typeof request?.ip === "string" ? request.ip : void 0) ?? request?.socket?.remoteAddress ?? "unknown";
887
+ return `${address}|${typeof email === "string" ? email.trim().toLowerCase() : ""}`;
888
+ }
889
+ __name(attemptKey, "attemptKey");
890
+ var Attempts = class Attempts2 {
891
+ static {
892
+ __name(this, "Attempts");
893
+ }
894
+ max;
895
+ seconds;
896
+ #failures = /* @__PURE__ */ new Map();
897
+ constructor(max, seconds) {
898
+ this.max = max;
899
+ this.seconds = seconds;
900
+ }
901
+ lockedOut(key) {
902
+ const entry = this.#failures.get(key);
903
+ if (!entry) return false;
904
+ if (entry.until === 0) return false;
905
+ if (entry.until > Date.now()) return true;
906
+ this.#failures.delete(key);
907
+ return false;
908
+ }
909
+ failed(key) {
910
+ const now = Date.now();
911
+ const existing = this.#failures.get(key);
912
+ const entry = existing && now - existing.since < this.seconds * 1e3 ? existing : {
913
+ count: 0,
914
+ since: now,
915
+ until: 0
916
+ };
917
+ entry.count += 1;
918
+ if (entry.count >= this.max) entry.until = now + this.seconds * 1e3;
919
+ this.#failures.set(key, entry);
920
+ if (this.#failures.size > 1e4) this.#prune();
921
+ }
922
+ succeeded(key) {
923
+ this.#failures.delete(key);
924
+ }
925
+ #prune() {
926
+ const now = Date.now();
927
+ for (const [key, entry] of this.#failures) {
928
+ const locked = entry.until > now;
929
+ const recent = now - entry.since < this.seconds * 1e3;
930
+ if (!locked && !recent) this.#failures.delete(key);
931
+ }
932
+ }
933
+ };
934
+
935
+ // src/dashboard/service.ts
936
+ var import_common5 = require("@nestjs/common");
937
+
938
+ // src/http/query-parser.ts
939
+ var FILTER_OPERATORS = [
940
+ "eq",
941
+ "ne",
942
+ "contains",
943
+ "startsWith",
944
+ "endsWith",
945
+ "gt",
946
+ "gte",
947
+ "lt",
948
+ "lte",
949
+ "in"
950
+ ];
951
+ var SORT_DIRECTIONS = /* @__PURE__ */ new Set([
952
+ "asc",
953
+ "desc"
954
+ ]);
955
+ function rejectStructuredValue(name, value) {
956
+ if (value === void 0 || value === null) return;
957
+ if (typeof value === "string") return;
958
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) return;
959
+ throw new InvalidQueryError(`"${name}" must be a plain value, not a nested structure. This API uses colon syntax - for example "?filter=age:gte:18" and "?sort=email:asc", not "?filter[age][gte]=18".`);
960
+ }
961
+ __name(rejectStructuredValue, "rejectStructuredValue");
962
+ var KNOWN_PARAMETERS = /* @__PURE__ */ new Set([
963
+ "page",
964
+ "perPage",
965
+ "search",
966
+ "sort",
967
+ "filter"
968
+ ]);
969
+ function rejectUnknownParameters(raw) {
970
+ const unknown = Object.keys(raw).filter((key) => !KNOWN_PARAMETERS.has(key));
971
+ if (unknown.length === 0) return;
972
+ const looksBracketed = unknown.some((key) => key.includes("["));
973
+ const hint = looksBracketed ? ' This API uses colon syntax: "?filter=age:gte:18", not "?filter[age][gte]=18".' : "";
974
+ throw new InvalidQueryError(`Unknown query parameter${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Supported: ${[
975
+ ...KNOWN_PARAMETERS
976
+ ].join(", ")}.${hint}`);
977
+ }
978
+ __name(rejectUnknownParameters, "rejectUnknownParameters");
979
+ function toStringList(name, value) {
980
+ rejectStructuredValue(name, value);
981
+ if (value === void 0 || value === null) return [];
982
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
983
+ return typeof value === "string" ? [
984
+ value
985
+ ] : [];
986
+ }
987
+ __name(toStringList, "toStringList");
988
+ function toSingleString(name, value) {
989
+ rejectStructuredValue(name, value);
990
+ if (typeof value === "string") return value;
991
+ if (Array.isArray(value)) {
992
+ const strings = value.filter((item) => typeof item === "string");
993
+ return strings.at(-1);
994
+ }
995
+ return void 0;
996
+ }
997
+ __name(toSingleString, "toSingleString");
998
+ function parsePositiveInteger(raw, name) {
999
+ if (raw === void 0 || raw === "") return void 0;
1000
+ if (!/^\d+$/.test(raw)) {
1001
+ throw new InvalidQueryError(`"${name}" must be a positive integer, received ${JSON.stringify(raw)}.`);
1002
+ }
1003
+ const parsed = Number(raw);
1004
+ if (parsed < 1) {
1005
+ throw new InvalidQueryError(`"${name}" must be >= 1, received ${JSON.stringify(raw)}.`);
1006
+ }
1007
+ return parsed;
1008
+ }
1009
+ __name(parsePositiveInteger, "parsePositiveInteger");
1010
+ function parseSort(raw) {
1011
+ const entries = toStringList("sort", raw).filter((entry) => entry.trim() !== "");
1012
+ if (entries.length === 0) return void 0;
1013
+ return entries.map((entry) => {
1014
+ const separator = entry.lastIndexOf(":");
1015
+ if (separator <= 0 || separator === entry.length - 1) {
1016
+ throw new InvalidQueryError(`Invalid sort "${entry}". Expected "field:asc" or "field:desc".`);
1017
+ }
1018
+ const field = entry.slice(0, separator);
1019
+ const direction = entry.slice(separator + 1);
1020
+ if (!SORT_DIRECTIONS.has(direction)) {
1021
+ throw new InvalidQueryError(`Invalid sort direction "${direction}" in "${entry}". Expected "asc" or "desc".`);
1022
+ }
1023
+ return {
1024
+ field,
1025
+ direction
1026
+ };
1027
+ });
1028
+ }
1029
+ __name(parseSort, "parseSort");
1030
+ function coerceScalar(raw, field, context) {
1031
+ if (!field) return raw;
1032
+ switch (field.kind) {
1033
+ case "number": {
1034
+ const parsed = Number(raw);
1035
+ if (raw.trim() === "" || !Number.isFinite(parsed)) {
1036
+ throw new InvalidQueryError(`${context}: "${raw}" is not a valid number.`);
1037
+ }
1038
+ return parsed;
1039
+ }
1040
+ case "boolean": {
1041
+ if (raw === "true") return true;
1042
+ if (raw === "false") return false;
1043
+ throw new InvalidQueryError(`${context}: "${raw}" is not a valid boolean (use true or false).`);
1044
+ }
1045
+ case "datetime": {
1046
+ const parsed = new Date(raw);
1047
+ if (Number.isNaN(parsed.getTime())) {
1048
+ throw new InvalidQueryError(`${context}: "${raw}" is not a valid date.`);
1049
+ }
1050
+ return parsed;
1051
+ }
1052
+ default:
1053
+ return raw;
1054
+ }
1055
+ }
1056
+ __name(coerceScalar, "coerceScalar");
1057
+ function coerceList(raw, field, context) {
1058
+ if (raw === "") return [];
1059
+ return raw.split(",").map((part) => coerceScalar(part, field, context));
1060
+ }
1061
+ __name(coerceList, "coerceList");
1062
+ function parseFilters(raw, model) {
1063
+ const entries = toStringList("filter", raw).filter((entry) => entry.trim() !== "");
1064
+ if (entries.length === 0) return void 0;
1065
+ return entries.map((entry) => {
1066
+ const firstSeparator = entry.indexOf(":");
1067
+ const secondSeparator = firstSeparator === -1 ? -1 : entry.indexOf(":", firstSeparator + 1);
1068
+ if (firstSeparator <= 0 || secondSeparator === -1) {
1069
+ throw new InvalidQueryError(`Invalid filter "${entry}". Expected "field:operator:value", for example "email:contains:example.com".`);
1070
+ }
1071
+ const fieldName = entry.slice(0, firstSeparator);
1072
+ const operator = entry.slice(firstSeparator + 1, secondSeparator);
1073
+ const rawValue = entry.slice(secondSeparator + 1);
1074
+ if (!FILTER_OPERATORS.includes(operator)) {
1075
+ throw new InvalidQueryError(`Unknown filter operator "${operator}" in "${entry}". Supported operators: ${FILTER_OPERATORS.join(", ")}.`);
1076
+ }
1077
+ const field = model.fields.find((candidate) => candidate.name === fieldName);
1078
+ const context = `Filter "${entry}"`;
1079
+ const value = operator === "in" ? coerceList(rawValue, field, context) : coerceScalar(rawValue, field, context);
1080
+ return {
1081
+ field: fieldName,
1082
+ operator,
1083
+ value
1084
+ };
1085
+ });
1086
+ }
1087
+ __name(parseFilters, "parseFilters");
1088
+ function parseFilterExpression(entry, model) {
1089
+ const rules = parseFilters(entry, model);
1090
+ const rule = rules?.[0];
1091
+ if (rule === void 0) {
1092
+ throw new InvalidQueryError(`Expected a filter of the form "field:operator:value", got "${entry}".`);
1093
+ }
1094
+ return rule;
1095
+ }
1096
+ __name(parseFilterExpression, "parseFilterExpression");
1097
+ function parseListQuery(raw, model) {
1098
+ rejectUnknownParameters(raw);
1099
+ const page = parsePositiveInteger(toSingleString("page", raw["page"]), "page");
1100
+ const perPage = parsePositiveInteger(toSingleString("perPage", raw["perPage"]), "perPage");
1101
+ const sort = parseSort(raw["sort"]);
1102
+ const filters = parseFilters(raw["filter"], model);
1103
+ const search = toSingleString("search", raw["search"]);
1104
+ return {
1105
+ ...page !== void 0 ? {
1106
+ page
1107
+ } : {},
1108
+ ...perPage !== void 0 ? {
1109
+ perPage
1110
+ } : {},
1111
+ ...sort ? {
1112
+ sort
1113
+ } : {},
1114
+ ...filters ? {
1115
+ filters
1116
+ } : {},
1117
+ ...search !== void 0 && search !== "" ? {
1118
+ search
1119
+ } : {}
1120
+ };
1121
+ }
1122
+ __name(parseListQuery, "parseListQuery");
1123
+
1124
+ // src/dashboard/contract.ts
1125
+ function modelOf(widget) {
1126
+ return widget.kind === "stat" ? void 0 : widget.model;
1127
+ }
1128
+ __name(modelOf, "modelOf");
1129
+ function defaultSpan(widget) {
1130
+ switch (widget.kind) {
1131
+ // A number is small; a chart needs room to be read; a list is a column of
1132
+ // rows and looks thin at a quarter width.
1133
+ case "chart":
1134
+ return 2;
1135
+ case "list":
1136
+ return 2;
1137
+ default:
1138
+ return 1;
1139
+ }
1140
+ }
1141
+ __name(defaultSpan, "defaultSpan");
1142
+
1143
+ // src/dashboard/service.ts
1144
+ var logger2 = new import_common5.Logger("NestAdmin");
1145
+ var MAX_BUCKETS = 90;
1146
+ var DAY = 864e5;
1147
+ async function buildDashboard(input) {
1148
+ const declared = input.declared;
1149
+ const generated = declared === void 0 || declared.length === 0;
1150
+ const widgets = generated ? generateFrom(input.models, input.labels ?? {}) : declared;
1151
+ const visible = new Set(input.models.map((model) => model.name));
1152
+ const resolved = await Promise.all(widgets.filter((widget) => {
1153
+ const model = modelOf(widget);
1154
+ return model === void 0 || visible.has(model);
1155
+ }).map((widget, index) => resolve(widget, index, input)));
1156
+ return {
1157
+ widgets: resolved,
1158
+ generated
1159
+ };
1160
+ }
1161
+ __name(buildDashboard, "buildDashboard");
1162
+ function generateFrom(models, labels) {
1163
+ const labelOf = /* @__PURE__ */ __name((model) => labels[model.name] ?? model.name, "labelOf");
1164
+ const widgets = models.map((model) => ({
1165
+ kind: "count",
1166
+ title: labelOf(model),
1167
+ model: model.name
1168
+ }));
1169
+ const dated = models.filter((model) => createdFieldFor(model) !== void 0);
1170
+ const first = dated[0];
1171
+ if (first) {
1172
+ widgets.push({
1173
+ kind: "chart",
1174
+ title: `New ${labelOf(first)}`,
1175
+ description: "Over the last 30 days.",
1176
+ model: first.name,
1177
+ span: 2
1178
+ });
1179
+ }
1180
+ for (const model of dated.slice(0, 2)) {
1181
+ widgets.push({
1182
+ kind: "list",
1183
+ title: `Recent ${labelOf(model)}`,
1184
+ model: model.name,
1185
+ span: 2
1186
+ });
1187
+ }
1188
+ return widgets;
1189
+ }
1190
+ __name(generateFrom, "generateFrom");
1191
+ async function resolve(widget, index, input) {
1192
+ const base = {
1193
+ // Stable within one document, and only used as a React key: a title is not
1194
+ // unique and a position changes when a widget above it is dropped.
1195
+ id: `${widget.kind}-${index}`,
1196
+ kind: widget.kind,
1197
+ title: widget.title,
1198
+ span: widget.span ?? defaultSpan(widget),
1199
+ ...widget.description !== void 0 ? {
1200
+ description: widget.description
1201
+ } : {},
1202
+ ...widget.kind !== "stat" ? {
1203
+ model: widget.model
1204
+ } : {},
1205
+ ...widget.kind !== "stat" && widget.filter !== void 0 ? {
1206
+ filter: widget.filter
1207
+ } : {}
1208
+ };
1209
+ try {
1210
+ return {
1211
+ ...base,
1212
+ data: await dataFor(widget, input)
1213
+ };
1214
+ } catch (cause) {
1215
+ logger2.warn(`Dashboard widget "${widget.title}" failed: ${String(cause)}`);
1216
+ return {
1217
+ ...base,
1218
+ failed: true
1219
+ };
1220
+ }
1221
+ }
1222
+ __name(resolve, "resolve");
1223
+ async function dataFor(widget, input) {
1224
+ switch (widget.kind) {
1225
+ case "stat":
1226
+ return widget.load({
1227
+ context: input.context
1228
+ });
1229
+ case "count":
1230
+ return countOf(widget, input);
1231
+ case "list":
1232
+ return listOf(widget, input);
1233
+ case "chart":
1234
+ return chartOf(widget, input);
1235
+ }
1236
+ }
1237
+ __name(dataFor, "dataFor");
1238
+ async function countOf(widget, input) {
1239
+ const model = modelFor(widget.model, input);
1240
+ const declared = filtersFor(widget.filter, model);
1241
+ const page = await input.adapter.list(widget.model, {
1242
+ perPage: 1,
1243
+ ...declared.length > 0 ? {
1244
+ filters: declared
1245
+ } : {}
1246
+ });
1247
+ if (widget.compareDays === void 0) return {
1248
+ value: page.total
1249
+ };
1250
+ const created = createdFieldFor(model);
1251
+ if (created === void 0) return {
1252
+ value: page.total
1253
+ };
1254
+ const since = new Date(Date.now() - widget.compareDays * DAY).toISOString();
1255
+ const recent = await input.adapter.list(widget.model, {
1256
+ perPage: 1,
1257
+ filters: [
1258
+ ...declared,
1259
+ {
1260
+ field: created,
1261
+ operator: "gte",
1262
+ value: since
1263
+ }
1264
+ ]
1265
+ });
1266
+ const before = page.total - recent.total;
1267
+ return {
1268
+ value: page.total,
1269
+ // Everything is new when there was nothing before, and dividing by zero to
1270
+ // say so would produce Infinity on a brand-new install.
1271
+ ...before > 0 ? {
1272
+ delta: Math.round(recent.total / before * 100)
1273
+ } : {},
1274
+ hint: `${recent.total} in the last ${widget.compareDays} days`
1275
+ };
1276
+ }
1277
+ __name(countOf, "countOf");
1278
+ async function listOf(widget, input) {
1279
+ const model = modelFor(widget.model, input);
1280
+ const created = createdFieldFor(model);
1281
+ const label = displayFieldFor(model);
1282
+ const key = model.primaryKey[0] ?? "id";
1283
+ const declared = filtersFor(widget.filter, model);
1284
+ const page = await input.adapter.list(widget.model, {
1285
+ perPage: Math.min(widget.limit ?? 5, 10),
1286
+ // Newest first where the model says which those are; otherwise whatever
1287
+ // order the adapter returns, which is better than refusing to show a list.
1288
+ ...created ? {
1289
+ sort: [
1290
+ {
1291
+ field: created,
1292
+ direction: "desc"
1293
+ }
1294
+ ]
1295
+ } : {},
1296
+ ...declared.length > 0 ? {
1297
+ filters: declared
1298
+ } : {}
1299
+ });
1300
+ return {
1301
+ total: page.total,
1302
+ records: page.data.map((record) => ({
1303
+ id: String(record[key] ?? ""),
1304
+ label: readable(record[label]) ?? String(record[key] ?? "")
1305
+ }))
1306
+ };
1307
+ }
1308
+ __name(listOf, "listOf");
1309
+ async function chartOf(widget, input) {
1310
+ const model = modelFor(widget.model, input);
1311
+ const created = createdFieldFor(model);
1312
+ if (created === void 0) {
1313
+ throw new Error(`${widget.model} has no creation timestamp, so it cannot be charted.`);
1314
+ }
1315
+ const bucket = widget.bucket ?? "day";
1316
+ const count = Math.min(widget.buckets ?? 30, MAX_BUCKETS);
1317
+ const size = bucket === "day" ? DAY : bucket === "week" ? 7 * DAY : 30 * DAY;
1318
+ const declared = filtersFor(widget.filter, model);
1319
+ const now = Date.now();
1320
+ const starts = Array.from({
1321
+ length: count
1322
+ }, (_, index) => now - (count - index) * size);
1323
+ const points = await Promise.all(starts.map(async (start) => {
1324
+ const page = await input.adapter.list(widget.model, {
1325
+ perPage: 1,
1326
+ filters: [
1327
+ ...declared,
1328
+ {
1329
+ field: created,
1330
+ operator: "gte",
1331
+ value: new Date(start).toISOString()
1332
+ },
1333
+ {
1334
+ field: created,
1335
+ operator: "lt",
1336
+ value: new Date(start + size).toISOString()
1337
+ }
1338
+ ]
1339
+ });
1340
+ return {
1341
+ at: new Date(start).toISOString(),
1342
+ value: page.total
1343
+ };
1344
+ }));
1345
+ return {
1346
+ points,
1347
+ total: points.reduce((sum, point) => sum + point.value, 0)
1348
+ };
1349
+ }
1350
+ __name(chartOf, "chartOf");
1351
+ function modelFor(name, input) {
1352
+ const model = input.models.find((candidate) => candidate.name === name);
1353
+ if (model === void 0) throw new Error(`${name} is not an exposed model.`);
1354
+ return model;
1355
+ }
1356
+ __name(modelFor, "modelFor");
1357
+ function filtersFor(filter, model) {
1358
+ return filter === void 0 ? [] : [
1359
+ parseFilterExpression(filter, model)
1360
+ ];
1361
+ }
1362
+ __name(filtersFor, "filtersFor");
1363
+ function readable(value) {
1364
+ if (typeof value === "string" && value !== "") return value;
1365
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1366
+ return void 0;
1367
+ }
1368
+ __name(readable, "readable");
1369
+
1370
+ // src/admin/metadata.dto.ts
1371
+ var ALL_PERMITTED = {
1372
+ list: true,
1373
+ read: true,
1374
+ create: true,
1375
+ update: true,
1376
+ delete: true
1377
+ };
1378
+ function targetForeignKeyOf(field, models) {
1379
+ if (relationShape(field, models) !== "one-to-many") return void 0;
1380
+ return inverseRelationField(field, models)?.relation?.from;
1381
+ }
1382
+ __name(targetForeignKeyOf, "targetForeignKeyOf");
1383
+ function byOrder(items, orderOf) {
1384
+ return [
1385
+ ...items
1386
+ ].map((item, index) => ({
1387
+ item,
1388
+ index,
1389
+ order: orderOf(item)
1390
+ })).sort((a, b) => {
1391
+ if (a.order === b.order) return a.index - b.index;
1392
+ if (a.order === void 0) return 1;
1393
+ if (b.order === void 0) return -1;
1394
+ return a.order - b.order;
1395
+ }).map((entry) => entry.item);
1396
+ }
1397
+ __name(byOrder, "byOrder");
1398
+ function toFieldDto(field, modelName, models, overrides) {
1399
+ const override = fieldOverride(overrides, modelName, field.name);
1400
+ return {
1401
+ name: field.name,
1402
+ kind: field.kind,
1403
+ isId: field.isId,
1404
+ isRequired: field.isRequired,
1405
+ isUnique: field.isUnique,
1406
+ isList: field.isList,
1407
+ isGenerated: field.isGenerated,
1408
+ readOnly: isReadOnly(overrides, modelName, field),
1409
+ ...field.writeOnly === true ? {
1410
+ writeOnly: true
1411
+ } : {},
1412
+ ...override?.label !== void 0 ? {
1413
+ label: override.label
1414
+ } : {},
1415
+ ...override?.widget !== void 0 ? {
1416
+ widget: override.widget
1417
+ } : {},
1418
+ ...field.defaultValue !== void 0 ? {
1419
+ defaultValue: field.defaultValue
1420
+ } : {},
1421
+ ...field.enumValues ? {
1422
+ enumValues: [
1423
+ ...field.enumValues
1424
+ ]
1425
+ } : {},
1426
+ ...field.relation ? {
1427
+ relation: {
1428
+ targetModel: field.relation.targetModel,
1429
+ cardinality: field.relation.cardinality,
1430
+ ...field.relation.from !== void 0 ? {
1431
+ from: field.relation.from
1432
+ } : {},
1433
+ ...field.relation.to !== void 0 ? {
1434
+ to: field.relation.to
1435
+ } : {},
1436
+ ...relationShape(field, models) !== void 0 ? {
1437
+ shape: relationShape(field, models)
1438
+ } : {},
1439
+ ...detachBlockedReason(field, models) !== void 0 ? {
1440
+ detachBlocked: detachBlockedReason(field, models)
1441
+ } : {},
1442
+ ...targetForeignKeyOf(field, models) !== void 0 ? {
1443
+ targetForeignKey: targetForeignKeyOf(field, models)
1444
+ } : {}
1445
+ }
1446
+ } : {}
1447
+ };
1448
+ }
1449
+ __name(toFieldDto, "toFieldDto");
1450
+ function toMetadataDto(models, overrides, permissions, actions) {
1451
+ const present = new Set(models.map((model) => model.name));
1452
+ return {
1453
+ models: byOrder(models, (model) => overrides?.[model.name]?.order).map((model) => ({
1454
+ name: model.name,
1455
+ primaryKey: [
1456
+ ...model.primaryKey
1457
+ ],
1458
+ displayField: displayFieldFor(model),
1459
+ can: permissions?.get(model.name) ?? ALL_PERMITTED,
1460
+ actions: actions?.get(model.name) ?? [],
1461
+ ...overrides?.[model.name]?.label !== void 0 ? {
1462
+ label: overrides[model.name]?.label
1463
+ } : {},
1464
+ ...overrides?.[model.name]?.icon !== void 0 ? {
1465
+ icon: overrides[model.name]?.icon
1466
+ } : {},
1467
+ fields: byOrder(model.fields.filter((field) => !field.relation || present.has(field.relation.targetModel)), (field) => fieldOverride(overrides, model.name, field.name)?.order).map((field) => toFieldDto(field, model.name, models, overrides))
1468
+ }))
1469
+ };
1470
+ }
1471
+ __name(toMetadataDto, "toMetadataDto");
1472
+
1473
+ // src/admin/service.ts
1474
+ function _ts_decorate3(decorators, target, key, desc) {
1475
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1476
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
1477
+ r = Reflect.decorate(decorators, target, key, desc);
1478
+ } else {
1479
+ for (var i = decorators.length - 1; i >= 0; i--) {
1480
+ if (d = decorators[i]) {
1481
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1482
+ }
1483
+ }
1484
+ }
1485
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1486
+ }
1487
+ __name(_ts_decorate3, "_ts_decorate");
1488
+ function _ts_metadata2(metadataKey, metadataValue) {
1489
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
1490
+ return Reflect.metadata(metadataKey, metadataValue);
1491
+ }
1492
+ }
1493
+ __name(_ts_metadata2, "_ts_metadata");
1494
+ function _ts_param2(paramIndex, decorator) {
1495
+ return function(target, key) {
1496
+ decorator(target, key, paramIndex);
1497
+ };
1498
+ }
1499
+ __name(_ts_param2, "_ts_param");
1500
+ function readableFields(model) {
1501
+ return model.fields.filter((field) => field.writeOnly !== true);
1502
+ }
1503
+ __name(readableFields, "readableFields");
1504
+ var MAX_BULK_DELETE = 200;
1505
+ var AdminService = class {
1506
+ static {
1507
+ __name(this, "AdminService");
1508
+ }
1509
+ adapter;
1510
+ resourceAuth;
1511
+ resources;
1512
+ overrides;
1513
+ hooks;
1514
+ actions;
1515
+ auth;
1516
+ dashboard;
1517
+ constructor(adapter, resourceAuth, resources, overrides, hooks, actions, auth, dashboard) {
1518
+ this.adapter = adapter;
1519
+ this.resourceAuth = resourceAuth;
1520
+ this.resources = resources;
1521
+ this.overrides = overrides;
1522
+ this.hooks = hooks;
1523
+ this.actions = actions;
1524
+ this.auth = auth;
1525
+ this.dashboard = dashboard;
1526
+ }
1527
+ logger = new import_common6.Logger("NestAdmin");
1528
+ /**
1529
+ * Fail at boot on a selection that names a model the schema does not have.
1530
+ *
1531
+ * A typo in `exclude` leaves the model exposed - the opposite of what was
1532
+ * asked for, and invisible until someone finds the table in the admin. It
1533
+ * cannot be checked in `forRoot`, because the model list comes from the
1534
+ * adapter and asking for it is asynchronous; this is the first moment it can
1535
+ * be known, and it is still before the first request.
1536
+ */
1537
+ async onModuleInit() {
1538
+ const schema = await this.adapter.getModels();
1539
+ const known = schema.map((model) => model.name);
1540
+ const missingResources = unknownSelectionNames(schema, this.resources);
1541
+ if (missingResources.length > 0) {
1542
+ throw new Error(`AdminModule \`resources\` names ${missingResources.length === 1 ? "a model" : "models"} that the schema does not have: ${missingResources.join(", ")}. Known models: ${known.join(", ")}.`);
1543
+ }
1544
+ const missingOverrides = unknownOverrideNames(selectModels(schema, this.resources), this.overrides);
1545
+ if (missingOverrides.length > 0) {
1546
+ throw new Error(`AdminModule \`models\` names ${missingOverrides.length === 1 ? "a model or field" : "models or fields"} this admin does not have: ${missingOverrides.join(", ")}. A typo in \`hidden\` leaves the real column exposed, so this is an error rather than a warning.`);
1547
+ }
1548
+ await this.checkBuiltInAuth(selectModels(schema, this.resources));
1549
+ const unwritable = unwritableHiddenFields(selectModels(schema, this.resources), this.overrides);
1550
+ if (unwritable.length > 0) {
1551
+ const one = unwritable.length === 1;
1552
+ throw new Error(`AdminModule \`models\` hides ${unwritable.join(", ")}, ${one ? "which is a required field" : "which are required fields"} with no default. Hiding ${one ? "it" : "them"} leaves no way to supply a value, so every create would fail. Give the column a default, make it optional, or leave it visible.`);
1553
+ }
1554
+ }
1555
+ /**
1556
+ * Two things about the built-in authentication that are only knowable here.
1557
+ *
1558
+ * Warnings rather than boot failures, and the distinction is deliberate.
1559
+ * Both describe a *deployment* that is wrong rather than a configuration
1560
+ * that cannot work - and an admin that refuses to start because its account
1561
+ * table is empty is an admin nobody can seed, because the seed script
1562
+ * imports the module.
1563
+ */
1564
+ async checkBuiltInAuth(exposed) {
1565
+ const runtime = builtInRuntimeOf(this.auth);
1566
+ if (!runtime) return;
1567
+ const accountModel = runtime.store.describes;
1568
+ if (accountModel !== void 0 && exposed.some((model) => model.name === accountModel)) {
1569
+ this.logger.warn(`AdminModule exposes "${accountModel}" as a resource, and it is also where the admin keeps its own accounts. Anyone who may edit it can grant themselves anything the admin can do. Exclude it with resources: { exclude: ["${accountModel}"] }.`);
1570
+ }
1571
+ try {
1572
+ if (await runtime.store.count() === 0) {
1573
+ this.logger.warn("AdminModule is using builtInAuth() and the account store is empty, so nobody can sign in. Create the first account with hashAdminPassword().");
1574
+ }
1575
+ } catch (cause) {
1576
+ this.logger.warn(`Could not read the admin account store: ${String(cause)}`);
1577
+ }
1578
+ }
1579
+ /**
1580
+ * What the dashboard shows this principal.
1581
+ *
1582
+ * Authorized the way everything else is, and *before* anything is queried: a
1583
+ * widget over a model this principal may not list is absent from the
1584
+ * document, so a dashboard cannot become a way to count rows of a table
1585
+ * nobody would let you open.
1586
+ *
1587
+ * The exposed model list is passed in rather than looked up again inside, so
1588
+ * "which models does this person see" is answered once, here, by the same
1589
+ * code that answers it for the metadata document.
1590
+ */
1591
+ async getDashboard(context) {
1592
+ const models = await this.exposedModels();
1593
+ const permitted = [];
1594
+ for (const model of models) {
1595
+ if (await this.permits(context, model.name, "list")) permitted.push(model);
1596
+ }
1597
+ return buildDashboard({
1598
+ adapter: this.adapter,
1599
+ models: permitted,
1600
+ declared: this.dashboard,
1601
+ context,
1602
+ labels: Object.fromEntries(Object.entries(this.overrides ?? {}).map(([name, override]) => [
1603
+ name,
1604
+ override?.label
1605
+ ]))
1606
+ });
1607
+ }
1608
+ /**
1609
+ * The public metadata document a frontend renders resources from.
1610
+ *
1611
+ * Models the principal may not see are filtered out **before** mapping, so a
1612
+ * denied model never reaches the DTO at all - not its name, fields, relations,
1613
+ * primary key or enum values. The response is not "everything, minus some";
1614
+ * it is a description of the schema this principal has.
1615
+ */
1616
+ async getMetadata(context) {
1617
+ const models = await this.exposedModels();
1618
+ const visible = [];
1619
+ for (const model of models) {
1620
+ if (await this.isVisible(context, model.name)) visible.push(model);
1621
+ }
1622
+ return toMetadataDto(visible, this.overrides, await this.permissionsFor(context, visible), await this.actionsFor(context, visible));
1623
+ }
1624
+ /**
1625
+ * List records.
1626
+ *
1627
+ * Metadata is resolved first - it decides whether the model is part of this
1628
+ * admin at all - and authorization second, so a denied model still never
1629
+ * reaches `adapter.list`. Query parsing needs that metadata anyway: only the
1630
+ * schema knows whether `price` should arrive as a number or a string.
1631
+ */
1632
+ async list(context, model, rawQuery) {
1633
+ const metadata = await this.requireModel(model);
1634
+ await this.assertAllowed(context, model, "list");
1635
+ return this.projectPage(metadata, await this.adapter.list(model, this.scopeToFields(metadata, parseListQuery(rawQuery, metadata))));
1636
+ }
1637
+ /**
1638
+ * Fetch one record.
1639
+ *
1640
+ * The adapter returns `null` for a missing record; over HTTP that is a 404,
1641
+ * so it is turned into an error here rather than in the controller.
1642
+ */
1643
+ async findOne(context, model, id) {
1644
+ const metadata = await this.requireModel(model);
1645
+ await this.assertAllowed(context, model, "read");
1646
+ const record = await this.adapter.findOne(model, id);
1647
+ if (record === null) throw new RecordNotFoundError(model, id);
1648
+ return this.project(metadata, record);
1649
+ }
1650
+ async create(context, model, data) {
1651
+ const metadata = await this.requireModel(model);
1652
+ await this.assertAllowed(context, model, "create");
1653
+ this.assertWritable(metadata, data);
1654
+ const prepared = await this.runBefore(context, metadata, "beforeCreate", data);
1655
+ const created = await this.adapter.create(model, prepared);
1656
+ await this.runAfter(context, model, "afterCreate", {
1657
+ record: created
1658
+ });
1659
+ return this.project(metadata, created);
1660
+ }
1661
+ async update(context, model, id, data) {
1662
+ const metadata = await this.requireModel(model);
1663
+ await this.assertAllowed(context, model, "update");
1664
+ this.assertWritable(metadata, data);
1665
+ const prepared = await this.runBefore(context, metadata, "beforeUpdate", data, id);
1666
+ const updated = await this.adapter.update(model, id, prepared);
1667
+ await this.runAfter(context, model, "afterUpdate", {
1668
+ id,
1669
+ record: updated
1670
+ });
1671
+ return this.project(metadata, updated);
1672
+ }
1673
+ /**
1674
+ * Delete several records, and say what happened to each.
1675
+ *
1676
+ * ## Why this is a loop and not a `deleteMany`
1677
+ *
1678
+ * The adapter contract has no bulk delete, and giving it one would mean
1679
+ * every adapter had to have one. More to the point, hooks are per-record: an
1680
+ * application that refuses to delete a pinned post must still refuse it when
1681
+ * the post is one of forty checkboxes. A single `deleteMany` would step past
1682
+ * every one of those refusals at once, which is the opposite of what a
1683
+ * confirmation dialog leads someone to expect.
1684
+ *
1685
+ * ## Why a partial result is a success
1686
+ *
1687
+ * Deleting thirty records where two are still referenced is not a failed
1688
+ * request - twenty-eight rows are gone, and an error response would say
1689
+ * nothing about which. So the response is a 200 carrying both lists, and the
1690
+ * interface reports them. Nothing is rolled back, and `§ Known Limitations`
1691
+ * says so: this is not a transaction, exactly as hooks are not.
1692
+ */
1693
+ async deleteMany(context, model, ids) {
1694
+ await this.requireModel(model);
1695
+ await this.assertAllowed(context, model, "delete");
1696
+ if (ids.length === 0) {
1697
+ throw new InvalidQueryError('Deleting records requires a body of the form { "ids": [...] }.');
1698
+ }
1699
+ if (ids.length > MAX_BULK_DELETE) {
1700
+ throw new InvalidQueryError(`Refusing to delete ${ids.length} records in one request. The limit is ${MAX_BULK_DELETE}.`);
1701
+ }
1702
+ const before = this.hooks?.[model]?.beforeDelete;
1703
+ const deleted = [];
1704
+ const failed = [];
1705
+ for (const id of ids) {
1706
+ try {
1707
+ if (before) await before({
1708
+ context,
1709
+ model,
1710
+ id
1711
+ });
1712
+ await this.adapter.delete(model, id);
1713
+ await this.runAfter(context, model, "afterDelete", {
1714
+ id
1715
+ });
1716
+ deleted.push(id);
1717
+ } catch (cause) {
1718
+ failed.push({
1719
+ id,
1720
+ message: clientMessage(cause)
1721
+ });
1722
+ }
1723
+ }
1724
+ return {
1725
+ deleted,
1726
+ failed
1727
+ };
1728
+ }
1729
+ async delete(context, model, id) {
1730
+ await this.requireModel(model);
1731
+ await this.assertAllowed(context, model, "delete");
1732
+ const before = this.hooks?.[model]?.beforeDelete;
1733
+ if (before) await before({
1734
+ context,
1735
+ model,
1736
+ id
1737
+ });
1738
+ await this.adapter.delete(model, id);
1739
+ await this.runAfter(context, model, "afterDelete", {
1740
+ id
1741
+ });
1742
+ }
1743
+ /**
1744
+ * A page of the records on the far side of a to-many relation.
1745
+ *
1746
+ * Authorized against **both** models, and the distinction matters. Reading
1747
+ * `/User/u1/posts` returns Post records, so a principal who may read a User
1748
+ * but not list Posts must not receive them through the back door of a
1749
+ * relation. The parent decides whether this record may be opened at all; the
1750
+ * target decides whether its records may be listed.
1751
+ */
1752
+ async listRelated(context, model, id, relationField, rawQuery) {
1753
+ const parent = await this.requireModel(model);
1754
+ await this.assertAllowed(context, model, "read");
1755
+ const target = await this.requireRelationTarget(parent, relationField);
1756
+ await this.assertAllowed(context, target.name, "list");
1757
+ return this.projectPage(target, await this.adapter.listRelated(model, id, relationField, this.scopeToFields(target, parseListQuery(rawQuery, target))));
1758
+ }
1759
+ /**
1760
+ * Link an existing record to this one.
1761
+ *
1762
+ * Requires `update` on both models. Across a one-to-many the child's foreign
1763
+ * key is what actually changes, so permitting this with rights over the
1764
+ * parent alone would let someone edit records they cannot otherwise touch.
1765
+ */
1766
+ async attachRelated(context, model, id, relationField, targetId) {
1767
+ const target = await this.assertMayRelink(context, model, relationField);
1768
+ await this.assertAllowed(context, target.name, "update");
1769
+ await this.adapter.attachRelated(model, id, relationField, targetId);
1770
+ }
1771
+ /**
1772
+ * Unlink a record from this one, leaving both in place.
1773
+ *
1774
+ * Refused up front when the relation cannot be broken - a child whose foreign
1775
+ * key is required cannot exist without a parent, so there is nothing to
1776
+ * detach it to. Saying so is better than forwarding a constraint violation.
1777
+ */
1778
+ async detachRelated(context, model, id, relationField, targetId) {
1779
+ const target = await this.assertMayRelink(context, model, relationField);
1780
+ await this.assertAllowed(context, target.name, "update");
1781
+ const parent = await this.requireModel(model);
1782
+ const field = parent.fields.find((candidate) => candidate.name === relationField);
1783
+ const blocked = field ? detachBlockedReason(field, await this.exposedModels()) : void 0;
1784
+ if (blocked) throw new InvalidQueryError(blocked);
1785
+ await this.adapter.detachRelated(model, id, relationField, targetId);
1786
+ }
1787
+ /** Shared preamble for attach and detach: the parent must be updatable. */
1788
+ async assertMayRelink(context, model, relationField) {
1789
+ const parent = await this.requireModel(model);
1790
+ await this.assertAllowed(context, model, "update");
1791
+ return this.requireRelationTarget(parent, relationField);
1792
+ }
1793
+ /**
1794
+ * The model on the far side of a to-many relation field.
1795
+ *
1796
+ * Resolved through the exposed set, so a relation pointing at a model this
1797
+ * admin does not expose reads as an unknown field rather than as a route
1798
+ * into it.
1799
+ */
1800
+ async requireRelationTarget(parent, relationField) {
1801
+ const field = parent.fields.find((candidate) => candidate.name === relationField);
1802
+ if (!field?.relation || field.relation.cardinality !== "many") {
1803
+ throw new FieldNotFoundError(parent.name, relationField, "Only a to-many relation can be listed this way.");
1804
+ }
1805
+ const target = (await this.exposedModels()).find((candidate) => candidate.name === field.relation?.targetModel);
1806
+ if (!target) throw new FieldNotFoundError(parent.name, relationField);
1807
+ return target;
1808
+ }
1809
+ /**
1810
+ * A record as this admin is allowed to return it.
1811
+ *
1812
+ * A whitelist against the effective metadata, which is what makes `hidden`
1813
+ * a guarantee rather than a request. The adapter reads whole rows - it knows
1814
+ * nothing about admin configuration - so a hidden column arrives here and is
1815
+ * dropped before anything can serialise it.
1816
+ *
1817
+ * Whitelisting rather than deleting the hidden names also covers a column the
1818
+ * adapter reports that the metadata does not describe: if it is not part of
1819
+ * this admin, it does not leave it.
1820
+ */
1821
+ project(model, record) {
1822
+ const allowed = new Set(readableFields(model).map((field) => field.name));
1823
+ const projected = {};
1824
+ for (const [key, value] of Object.entries(record)) {
1825
+ if (allowed.has(key)) projected[key] = value;
1826
+ }
1827
+ return projected;
1828
+ }
1829
+ /**
1830
+ * Tell the adapter which fields this admin exposes.
1831
+ *
1832
+ * The adapter reads a schema, not a configuration, so without this a hidden
1833
+ * column would still be searched by free text, sorted and filtered on, and
1834
+ * read from the database - each of them a way to learn a value nobody is
1835
+ * meant to see. `project` would still keep it out of the response, but
1836
+ * "you cannot read it" is a weaker promise than "it was never fetched".
1837
+ */
1838
+ /**
1839
+ * Which columns the adapter is allowed to return.
1840
+ *
1841
+ * Not every field the model has: a `writeOnly` one is accepted on a write and
1842
+ * must never come back, so it is left out of the query itself rather than
1843
+ * removed from the answer afterwards. The projection below removes it a
1844
+ * second time, which is deliberate - see `FieldMetadata.writeOnly`.
1845
+ */
1846
+ scopeToFields(model, query) {
1847
+ return {
1848
+ ...query,
1849
+ fields: readableFields(model).map((field) => field.name)
1850
+ };
1851
+ }
1852
+ projectPage(model, page) {
1853
+ return {
1854
+ ...page,
1855
+ data: page.data.map((record) => this.project(model, record))
1856
+ };
1857
+ }
1858
+ /**
1859
+ * Reject a write that names a field this admin will not write.
1860
+ *
1861
+ * The adapter validates too, but against the *schema* - it would accept a
1862
+ * hidden or read-only column, because from where it stands those are ordinary
1863
+ * writable ones. This is the only layer that knows the difference.
1864
+ */
1865
+ assertWritable(model, data) {
1866
+ for (const key of Object.keys(data)) {
1867
+ const field = model.fields.find((candidate) => candidate.name === key);
1868
+ if (!field) throw new FieldNotFoundError(model.name, key);
1869
+ if (isReadOnly(this.overrides, model.name, field)) {
1870
+ throw new FieldNotFoundError(model.name, key, field.isGenerated ? "This value is produced by the database." : "This field is configured as read-only.");
1871
+ }
1872
+ }
1873
+ }
1874
+ /**
1875
+ * What this principal may do with each visible model.
1876
+ *
1877
+ * Asked of the same policy the requests go through, so the document and the
1878
+ * enforcement cannot disagree. A policy that throws `ForbiddenError` is read
1879
+ * as a denial, exactly as `isVisible` reads it; anything else it throws is a
1880
+ * bug and propagates.
1881
+ *
1882
+ * Without this the interface offers `New`, `Edit` and `Delete` to a
1883
+ * principal for whom every one of them would be refused - a button that
1884
+ * exists only to produce a 403 is worse than no button.
1885
+ */
1886
+ async permissionsFor(context, models) {
1887
+ const permissions = /* @__PURE__ */ new Map();
1888
+ for (const model of models) {
1889
+ const permits = /* @__PURE__ */ __name(async (operation) => this.permits(context, model.name, operation), "permits");
1890
+ permissions.set(model.name, {
1891
+ list: await permits("list"),
1892
+ read: await permits("read"),
1893
+ create: await permits("create"),
1894
+ update: await permits("update"),
1895
+ delete: await permits("delete")
1896
+ });
1897
+ }
1898
+ return permissions;
1899
+ }
1900
+ /** The policy's answer for one operation, with a thrown denial read as `false`. */
1901
+ async permits(context, model, operation) {
1902
+ try {
1903
+ return await this.resourceAuth.authorize({
1904
+ context,
1905
+ model,
1906
+ operation
1907
+ }) !== false;
1908
+ } catch (error) {
1909
+ if (isNestAdminError(error) && error.kind === "forbidden") return false;
1910
+ throw error;
1911
+ }
1912
+ }
1913
+ /**
1914
+ * Run a `before` hook, if the model has one.
1915
+ *
1916
+ * The result is validated again rather than trusted: a hook is application
1917
+ * code, and the rule that a hidden or read-only field cannot be written is
1918
+ * not one it should be able to step around by accident.
1919
+ */
1920
+ async runBefore(context, metadata, hook, data, id) {
1921
+ const handler = this.hooks?.[metadata.name]?.[hook];
1922
+ if (!handler) return data;
1923
+ const result = await (hook === "beforeCreate" ? handler({
1924
+ context,
1925
+ model: metadata.name,
1926
+ data
1927
+ }) : handler({
1928
+ context,
1929
+ model: metadata.name,
1930
+ id,
1931
+ data
1932
+ }));
1933
+ this.assertWritable(metadata, result);
1934
+ return result;
1935
+ }
1936
+ /**
1937
+ * Run an `after` hook, if the model has one.
1938
+ *
1939
+ * Nothing is rolled back if it throws - the write already happened - so the
1940
+ * failure is reported as it is rather than dressed up as a failed write.
1941
+ */
1942
+ async runAfter(context, model, hook, args) {
1943
+ const handler = this.hooks?.[model]?.[hook];
1944
+ if (!handler) return;
1945
+ await handler({
1946
+ context,
1947
+ model,
1948
+ ...args
1949
+ });
1950
+ }
1951
+ /**
1952
+ * The actions this principal may run, per model.
1953
+ *
1954
+ * Filtered by the policy before it reaches the document, so an action that
1955
+ * would be refused is simply not there - the interface cannot draw a button
1956
+ * for something it was never told about.
1957
+ */
1958
+ async actionsFor(context, models) {
1959
+ const byModel = /* @__PURE__ */ new Map();
1960
+ for (const model of models) {
1961
+ const declared = this.actions?.[model.name] ?? [];
1962
+ if (declared.length === 0) continue;
1963
+ if (!await this.permits(context, model.name, "action")) continue;
1964
+ byModel.set(model.name, declared.map((action) => ({
1965
+ name: action.name,
1966
+ label: action.label ?? action.name,
1967
+ scope: action.scope,
1968
+ ...action.confirm !== void 0 ? {
1969
+ confirm: action.confirm
1970
+ } : {},
1971
+ ...action.danger !== void 0 ? {
1972
+ danger: action.danger
1973
+ } : {}
1974
+ })));
1975
+ }
1976
+ return byModel;
1977
+ }
1978
+ /**
1979
+ * Run one application-defined action.
1980
+ *
1981
+ * Authorized as `'action'` rather than as the operation it resembles: an
1982
+ * action can do anything, so a policy should be able to decide about it on
1983
+ * its own terms.
1984
+ *
1985
+ * A `'record'` action is given the id; a `'list'` one is not, and passing an
1986
+ * id to it - or omitting one from a record action - is a request that does
1987
+ * not match the action that was declared.
1988
+ */
1989
+ async runAction(context, model, name, id) {
1990
+ await this.requireModel(model);
1991
+ await this.assertAllowed(context, model, "action");
1992
+ const action = (this.actions?.[model] ?? []).find((candidate) => candidate.name === name);
1993
+ if (!action) {
1994
+ throw new FieldNotFoundError(model, name, "No such action.");
1995
+ }
1996
+ if (action.scope === "record" && id === void 0) {
1997
+ throw new InvalidQueryError(`Action "${name}" applies to one record and needs an id.`);
1998
+ }
1999
+ if (action.scope === "list" && id !== void 0) {
2000
+ throw new InvalidQueryError(`Action "${name}" applies to the whole model, not to a record.`);
2001
+ }
2002
+ return await action.run({
2003
+ context,
2004
+ model,
2005
+ ...id === void 0 ? {} : {
2006
+ id
2007
+ }
2008
+ }) ?? {};
2009
+ }
2010
+ // ------------------------------------------------------- resource policy
2011
+ /**
2012
+ * Deny the request unless the policy permits this operation on this model.
2013
+ *
2014
+ * Called before any adapter operation. Both a `false` return and a thrown
2015
+ * `ForbiddenError` mean the same thing here, so a host may use whichever
2016
+ * reads better. Anything else the policy throws propagates untouched and the
2017
+ * exception filter turns it into a generic 500 - a broken policy fails the
2018
+ * request rather than quietly allowing it.
2019
+ */
2020
+ async assertAllowed(context, model, operation) {
2021
+ const decision = await this.resourceAuth.authorize({
2022
+ context,
2023
+ model,
2024
+ operation
2025
+ });
2026
+ if (decision === false) throw new ForbiddenError();
2027
+ }
2028
+ /**
2029
+ * Is this model visible in the metadata document?
2030
+ *
2031
+ * Same policy, different consequence. A denial here must **hide** the model
2032
+ * rather than fail the request: surfacing a 403 from `GET /admin/meta` would
2033
+ * tell the caller that a model they cannot see exists, which is the side
2034
+ * channel this whole phase is meant to close.
2035
+ *
2036
+ * A `ForbiddenError` is therefore caught and read as "not visible". Any other
2037
+ * error is rethrown - a bug in the policy must surface as a 500, not silently
2038
+ * reshape the schema a client is shown.
2039
+ */
2040
+ async isVisible(context, model) {
2041
+ try {
2042
+ const decision = await this.resourceAuth.authorize({
2043
+ context,
2044
+ model,
2045
+ operation: "metadata"
2046
+ });
2047
+ return decision !== false;
2048
+ } catch (error) {
2049
+ if (isNestAdminError(error) && error.kind === "forbidden") return false;
2050
+ throw error;
2051
+ }
2052
+ }
2053
+ /**
2054
+ * The models this admin exposes, after the configured selection.
2055
+ *
2056
+ * Every path goes through here, so an excluded model is absent from the
2057
+ * metadata document and unknown to every route.
2058
+ */
2059
+ async exposedModels() {
2060
+ return applyOverrides(selectModels(await this.adapter.getModels(), this.resources), this.overrides);
2061
+ }
2062
+ /**
2063
+ * Resolve a model name to its metadata, or fail with 404.
2064
+ *
2065
+ * Called before the policy on every operation, and that order is deliberate.
2066
+ * Whether a model exists is structural - the same answer for everyone - so an
2067
+ * excluded model answers 404 rather than 403, and does so identically for
2068
+ * every principal. Asking the policy first would make a model that is not
2069
+ * part of this admin look like one the caller merely lacks access to.
2070
+ */
2071
+ async requireModel(model) {
2072
+ const models = await this.exposedModels();
2073
+ const found = models.find((candidate) => candidate.name === model);
2074
+ if (!found) {
2075
+ throw new ModelNotFoundError(model, models.map((candidate) => candidate.name));
2076
+ }
2077
+ return found;
2078
+ }
2079
+ };
2080
+ AdminService = _ts_decorate3([
2081
+ (0, import_common6.Injectable)(),
2082
+ _ts_param2(0, (0, import_common6.Inject)(ADMIN_ADAPTER)),
2083
+ _ts_param2(1, (0, import_common6.Inject)(ADMIN_RESOURCE_AUTH)),
2084
+ _ts_param2(2, (0, import_common6.Inject)(ADMIN_RESOURCES)),
2085
+ _ts_param2(3, (0, import_common6.Inject)(ADMIN_MODELS)),
2086
+ _ts_param2(4, (0, import_common6.Inject)(ADMIN_HOOKS)),
2087
+ _ts_param2(5, (0, import_common6.Inject)(ADMIN_ACTIONS)),
2088
+ _ts_param2(6, (0, import_common6.Inject)(ADMIN_AUTH)),
2089
+ _ts_param2(7, (0, import_common6.Inject)(ADMIN_DASHBOARD)),
2090
+ _ts_metadata2("design:type", Function),
2091
+ _ts_metadata2("design:paramtypes", [
2092
+ typeof OrmAdapter === "undefined" ? Object : OrmAdapter,
2093
+ typeof AdminResourceAuth === "undefined" ? Object : AdminResourceAuth,
2094
+ Object,
2095
+ Object,
2096
+ Object,
2097
+ Object,
2098
+ typeof AdminAuth === "undefined" ? Object : AdminAuth,
2099
+ Object
2100
+ ])
2101
+ ], AdminService);
2102
+
2103
+ // src/admin/controller.ts
2104
+ function _ts_decorate4(decorators, target, key, desc) {
2105
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2106
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2107
+ r = Reflect.decorate(decorators, target, key, desc);
2108
+ } else {
2109
+ for (var i = decorators.length - 1; i >= 0; i--) {
2110
+ if (d = decorators[i]) {
2111
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2112
+ }
2113
+ }
2114
+ }
2115
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2116
+ }
2117
+ __name(_ts_decorate4, "_ts_decorate");
2118
+ function _ts_metadata3(metadataKey, metadataValue) {
2119
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
2120
+ return Reflect.metadata(metadataKey, metadataValue);
2121
+ }
2122
+ }
2123
+ __name(_ts_metadata3, "_ts_metadata");
2124
+ function _ts_param3(paramIndex, decorator) {
2125
+ return function(target, key) {
2126
+ decorator(target, key, paramIndex);
2127
+ };
2128
+ }
2129
+ __name(_ts_param3, "_ts_param");
2130
+ var AdminController = class {
2131
+ static {
2132
+ __name(this, "AdminController");
2133
+ }
2134
+ service;
2135
+ constructor(service) {
2136
+ this.service = service;
2137
+ }
2138
+ /**
2139
+ * Declared before `:model` so the literal segment wins. A model named
2140
+ * exactly `meta` would be shadowed; Prisma model names are conventionally
2141
+ * capitalised (`Meta`) and matching is case-sensitive, so this is a narrow
2142
+ * and documented corner.
2143
+ */
2144
+ /**
2145
+ * `POST /admin/actions/:model/:action[/:id]` - run an application action.
2146
+ *
2147
+ * Under a reserved first segment, and declared before every `:model` route,
2148
+ * so `actions` is matched literally. The same arrangement already reserves
2149
+ * `meta` here and `assets` in the UI controller; the cost is that a model
2150
+ * called `actions` would be unreachable, which is documented rather than
2151
+ * guarded against.
2152
+ */
2153
+ async runListAction(context, model, action) {
2154
+ return success(await this.service.runAction(context, model, action));
2155
+ }
2156
+ async runRecordAction(context, model, action, id) {
2157
+ return success(await this.service.runAction(context, model, action, id));
2158
+ }
2159
+ async meta(context) {
2160
+ return success(await this.service.getMetadata(context));
2161
+ }
2162
+ /**
2163
+ * `GET /admin/dashboard` - what the landing page shows.
2164
+ *
2165
+ * Declared before `:model` so the literal segment wins, as `meta` and
2166
+ * `actions` already are. The cost is the same and is documented with them: a
2167
+ * model named `dashboard` would be unreachable.
2168
+ */
2169
+ async dashboard(context) {
2170
+ return success(await this.service.getDashboard(context));
2171
+ }
2172
+ async list(context, model, query) {
2173
+ const page = await this.service.list(context, model, query);
2174
+ return successPage(page.data, {
2175
+ total: page.total,
2176
+ page: page.page,
2177
+ perPage: page.perPage
2178
+ });
2179
+ }
2180
+ async findOne(context, model, id) {
2181
+ return success(await this.service.findOne(context, model, id));
2182
+ }
2183
+ async create(context, model, body) {
2184
+ return success(await this.service.create(context, model, body));
2185
+ }
2186
+ async update(context, model, id, body) {
2187
+ return success(await this.service.update(context, model, id, body));
2188
+ }
2189
+ /**
2190
+ * Returns 200 with a `null` payload rather than 204, so every admin endpoint
2191
+ * answers with the same envelope and a client needs one response shape.
2192
+ */
2193
+ async remove(context, model, id) {
2194
+ await this.service.delete(context, model, id);
2195
+ return success(null);
2196
+ }
2197
+ /**
2198
+ * `DELETE /admin/:model` with `{ "ids": [...] }` - delete several records.
2199
+ *
2200
+ * One segment, so it cannot be confused with `/:model/:id`. The ids are in
2201
+ * the body rather than the query string because a selection of two hundred
2202
+ * would not survive a URL length limit, and a request that silently deletes
2203
+ * the first N of what was asked for is worse than one that fails.
2204
+ *
2205
+ * Answers 200 with both lists even when some records survived; see
2206
+ * `deleteMany` for why a partial result is not an error.
2207
+ */
2208
+ async removeMany(context, model, body) {
2209
+ const ids = body?.ids;
2210
+ if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string" && typeof id !== "number")) {
2211
+ throw new InvalidQueryError('Deleting records requires a body of the form { "ids": ["...", "..."] }.');
2212
+ }
2213
+ return success(await this.service.deleteMany(context, model, ids));
2214
+ }
2215
+ /**
2216
+ * `GET /admin/:model/:id/:relation` - a page of related records.
2217
+ *
2218
+ * Three segments, so it cannot be confused with `/:model/:id`. The query
2219
+ * string is the ordinary list query and describes the records being returned,
2220
+ * not the record they hang off.
2221
+ */
2222
+ async listRelated(context, model, id, relation, query) {
2223
+ const page = await this.service.listRelated(context, model, id, relation, query);
2224
+ return successPage(page.data, {
2225
+ total: page.total,
2226
+ page: page.page,
2227
+ perPage: page.perPage
2228
+ });
2229
+ }
2230
+ /**
2231
+ * `POST /admin/:model/:id/:relation` with `{ "id": "..." }` - link a record.
2232
+ *
2233
+ * The body carries only an id: this attaches something that already exists.
2234
+ * Creating a record and linking it in one request is a different operation
2235
+ * and is not this one.
2236
+ */
2237
+ async attachRelated(context, model, id, relation, body) {
2238
+ const targetId = body?.id;
2239
+ if (typeof targetId !== "string" && typeof targetId !== "number") {
2240
+ throw new InvalidQueryError('Attaching a related record requires a body of the form { "id": "..." }.');
2241
+ }
2242
+ await this.service.attachRelated(context, model, id, relation, targetId);
2243
+ return success(null);
2244
+ }
2245
+ /** `DELETE /admin/:model/:id/:relation/:targetId` - unlink, without deleting. */
2246
+ async detachRelated(context, model, id, relation, targetId) {
2247
+ await this.service.detachRelated(context, model, id, relation, targetId);
2248
+ return success(null);
2249
+ }
2250
+ };
2251
+ _ts_decorate4([
2252
+ (0, import_common7.Post)("actions/:model/:action"),
2253
+ _ts_param3(0, AdminContext()),
2254
+ _ts_param3(1, (0, import_common7.Param)("model")),
2255
+ _ts_param3(2, (0, import_common7.Param)("action")),
2256
+ _ts_metadata3("design:type", Function),
2257
+ _ts_metadata3("design:paramtypes", [
2258
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2259
+ String,
2260
+ String
2261
+ ]),
2262
+ _ts_metadata3("design:returntype", Promise)
2263
+ ], AdminController.prototype, "runListAction", null);
2264
+ _ts_decorate4([
2265
+ (0, import_common7.Post)("actions/:model/:action/:id"),
2266
+ _ts_param3(0, AdminContext()),
2267
+ _ts_param3(1, (0, import_common7.Param)("model")),
2268
+ _ts_param3(2, (0, import_common7.Param)("action")),
2269
+ _ts_param3(3, (0, import_common7.Param)("id")),
2270
+ _ts_metadata3("design:type", Function),
2271
+ _ts_metadata3("design:paramtypes", [
2272
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2273
+ String,
2274
+ String,
2275
+ String
2276
+ ]),
2277
+ _ts_metadata3("design:returntype", Promise)
2278
+ ], AdminController.prototype, "runRecordAction", null);
2279
+ _ts_decorate4([
2280
+ (0, import_common7.Get)("meta"),
2281
+ _ts_param3(0, AdminContext()),
2282
+ _ts_metadata3("design:type", Function),
2283
+ _ts_metadata3("design:paramtypes", [
2284
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext
2285
+ ]),
2286
+ _ts_metadata3("design:returntype", Promise)
2287
+ ], AdminController.prototype, "meta", null);
2288
+ _ts_decorate4([
2289
+ (0, import_common7.Get)("dashboard"),
2290
+ _ts_param3(0, AdminContext()),
2291
+ _ts_metadata3("design:type", Function),
2292
+ _ts_metadata3("design:paramtypes", [
2293
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext
2294
+ ]),
2295
+ _ts_metadata3("design:returntype", Promise)
2296
+ ], AdminController.prototype, "dashboard", null);
2297
+ _ts_decorate4([
2298
+ (0, import_common7.Get)(":model"),
2299
+ _ts_param3(0, AdminContext()),
2300
+ _ts_param3(1, (0, import_common7.Param)("model")),
2301
+ _ts_param3(2, (0, import_common7.Query)()),
2302
+ _ts_metadata3("design:type", Function),
2303
+ _ts_metadata3("design:paramtypes", [
2304
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2305
+ String,
2306
+ typeof RawQuery === "undefined" ? Object : RawQuery
2307
+ ]),
2308
+ _ts_metadata3("design:returntype", Promise)
2309
+ ], AdminController.prototype, "list", null);
2310
+ _ts_decorate4([
2311
+ (0, import_common7.Get)(":model/:id"),
2312
+ _ts_param3(0, AdminContext()),
2313
+ _ts_param3(1, (0, import_common7.Param)("model")),
2314
+ _ts_param3(2, (0, import_common7.Param)("id")),
2315
+ _ts_metadata3("design:type", Function),
2316
+ _ts_metadata3("design:paramtypes", [
2317
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2318
+ String,
2319
+ String
2320
+ ]),
2321
+ _ts_metadata3("design:returntype", Promise)
2322
+ ], AdminController.prototype, "findOne", null);
2323
+ _ts_decorate4([
2324
+ (0, import_common7.Post)(":model"),
2325
+ _ts_param3(0, AdminContext()),
2326
+ _ts_param3(1, (0, import_common7.Param)("model")),
2327
+ _ts_param3(2, (0, import_common7.Body)()),
2328
+ _ts_metadata3("design:type", Function),
2329
+ _ts_metadata3("design:paramtypes", [
2330
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2331
+ String,
2332
+ typeof RecordData === "undefined" ? Object : RecordData
2333
+ ]),
2334
+ _ts_metadata3("design:returntype", Promise)
2335
+ ], AdminController.prototype, "create", null);
2336
+ _ts_decorate4([
2337
+ (0, import_common7.Patch)(":model/:id"),
2338
+ _ts_param3(0, AdminContext()),
2339
+ _ts_param3(1, (0, import_common7.Param)("model")),
2340
+ _ts_param3(2, (0, import_common7.Param)("id")),
2341
+ _ts_param3(3, (0, import_common7.Body)()),
2342
+ _ts_metadata3("design:type", Function),
2343
+ _ts_metadata3("design:paramtypes", [
2344
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2345
+ String,
2346
+ String,
2347
+ typeof RecordData === "undefined" ? Object : RecordData
2348
+ ]),
2349
+ _ts_metadata3("design:returntype", Promise)
2350
+ ], AdminController.prototype, "update", null);
2351
+ _ts_decorate4([
2352
+ (0, import_common7.Delete)(":model/:id"),
2353
+ _ts_param3(0, AdminContext()),
2354
+ _ts_param3(1, (0, import_common7.Param)("model")),
2355
+ _ts_param3(2, (0, import_common7.Param)("id")),
2356
+ _ts_metadata3("design:type", Function),
2357
+ _ts_metadata3("design:paramtypes", [
2358
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2359
+ String,
2360
+ String
2361
+ ]),
2362
+ _ts_metadata3("design:returntype", Promise)
2363
+ ], AdminController.prototype, "remove", null);
2364
+ _ts_decorate4([
2365
+ (0, import_common7.Delete)(":model"),
2366
+ _ts_param3(0, AdminContext()),
2367
+ _ts_param3(1, (0, import_common7.Param)("model")),
2368
+ _ts_param3(2, (0, import_common7.Body)()),
2369
+ _ts_metadata3("design:type", Function),
2370
+ _ts_metadata3("design:paramtypes", [
2371
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2372
+ String,
2373
+ Object
2374
+ ]),
2375
+ _ts_metadata3("design:returntype", Promise)
2376
+ ], AdminController.prototype, "removeMany", null);
2377
+ _ts_decorate4([
2378
+ (0, import_common7.Get)(":model/:id/:relation"),
2379
+ _ts_param3(0, AdminContext()),
2380
+ _ts_param3(1, (0, import_common7.Param)("model")),
2381
+ _ts_param3(2, (0, import_common7.Param)("id")),
2382
+ _ts_param3(3, (0, import_common7.Param)("relation")),
2383
+ _ts_param3(4, (0, import_common7.Query)()),
2384
+ _ts_metadata3("design:type", Function),
2385
+ _ts_metadata3("design:paramtypes", [
2386
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2387
+ String,
2388
+ String,
2389
+ String,
2390
+ typeof RawQuery === "undefined" ? Object : RawQuery
2391
+ ]),
2392
+ _ts_metadata3("design:returntype", Promise)
2393
+ ], AdminController.prototype, "listRelated", null);
2394
+ _ts_decorate4([
2395
+ (0, import_common7.Post)(":model/:id/:relation"),
2396
+ _ts_param3(0, AdminContext()),
2397
+ _ts_param3(1, (0, import_common7.Param)("model")),
2398
+ _ts_param3(2, (0, import_common7.Param)("id")),
2399
+ _ts_param3(3, (0, import_common7.Param)("relation")),
2400
+ _ts_param3(4, (0, import_common7.Body)()),
2401
+ _ts_metadata3("design:type", Function),
2402
+ _ts_metadata3("design:paramtypes", [
2403
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2404
+ String,
2405
+ String,
2406
+ String,
2407
+ Object
2408
+ ]),
2409
+ _ts_metadata3("design:returntype", Promise)
2410
+ ], AdminController.prototype, "attachRelated", null);
2411
+ _ts_decorate4([
2412
+ (0, import_common7.Delete)(":model/:id/:relation/:targetId"),
2413
+ _ts_param3(0, AdminContext()),
2414
+ _ts_param3(1, (0, import_common7.Param)("model")),
2415
+ _ts_param3(2, (0, import_common7.Param)("id")),
2416
+ _ts_param3(3, (0, import_common7.Param)("relation")),
2417
+ _ts_param3(4, (0, import_common7.Param)("targetId")),
2418
+ _ts_metadata3("design:type", Function),
2419
+ _ts_metadata3("design:paramtypes", [
2420
+ typeof ExecutionContext === "undefined" ? Object : ExecutionContext,
2421
+ String,
2422
+ String,
2423
+ String,
2424
+ String
2425
+ ]),
2426
+ _ts_metadata3("design:returntype", Promise)
2427
+ ], AdminController.prototype, "detachRelated", null);
2428
+ AdminController = _ts_decorate4([
2429
+ (0, import_common7.Controller)(),
2430
+ (0, import_common7.UseGuards)(AdminAuthGuard),
2431
+ (0, import_common7.UseFilters)(AdminExceptionFilter),
2432
+ _ts_metadata3("design:type", Function),
2433
+ _ts_metadata3("design:paramtypes", [
2434
+ typeof AdminService === "undefined" ? Object : AdminService
2435
+ ])
2436
+ ], AdminController);
2437
+
2438
+ // src/auth/controller.ts
2439
+ var import_common8 = require("@nestjs/common");
2440
+ function _ts_decorate5(decorators, target, key, desc) {
2441
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2442
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2443
+ r = Reflect.decorate(decorators, target, key, desc);
2444
+ } else {
2445
+ for (var i = decorators.length - 1; i >= 0; i--) {
2446
+ if (d = decorators[i]) {
2447
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2448
+ }
2449
+ }
2450
+ }
2451
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2452
+ }
2453
+ __name(_ts_decorate5, "_ts_decorate");
2454
+ function _ts_metadata4(metadataKey, metadataValue) {
2455
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
2456
+ return Reflect.metadata(metadataKey, metadataValue);
2457
+ }
2458
+ }
2459
+ __name(_ts_metadata4, "_ts_metadata");
2460
+ function _ts_param4(paramIndex, decorator) {
2461
+ return function(target, key) {
2462
+ decorator(target, key, paramIndex);
2463
+ };
2464
+ }
2465
+ __name(_ts_param4, "_ts_param");
2466
+ var AdminAuthController = class {
2467
+ static {
2468
+ __name(this, "AdminAuthController");
2469
+ }
2470
+ auth;
2471
+ constructor(auth) {
2472
+ this.auth = auth;
2473
+ }
2474
+ /**
2475
+ * `GET /admin/auth/session` - who is signed in, if anyone.
2476
+ *
2477
+ * Answers `200` with `account: null` rather than `401` for an absent
2478
+ * session. The interface asks this before it has any reason to think anybody
2479
+ * is signed in, and an error is the wrong shape for "no, and that is fine" -
2480
+ * it would put a failure in the console on every visit to the login page.
2481
+ */
2482
+ async session(request) {
2483
+ const runtime = this.runtime();
2484
+ const token = cookieFrom(request?.headers?.cookie, runtime.cookieName);
2485
+ const id = token === void 0 ? void 0 : readSession(token, runtime.secret);
2486
+ if (id === void 0) return success({
2487
+ account: null
2488
+ });
2489
+ const account = await runtime.store.findById(id);
2490
+ return success({
2491
+ account: !account || account.disabled === true ? null : summarise(account)
2492
+ });
2493
+ }
2494
+ /**
2495
+ * `POST /admin/auth/login` with `{ email, password }`.
2496
+ *
2497
+ * `200` and a cookie, or `401` and nothing. There is exactly one failure
2498
+ * message: an unknown address, a wrong password, a disabled account and a
2499
+ * locked-out one are indistinguishable from outside. Telling them apart is
2500
+ * convenient for the person signing in perhaps twice a year, and a list of
2501
+ * which addresses are registered for everyone else.
2502
+ */
2503
+ async login(request, response, body) {
2504
+ const runtime = this.runtime();
2505
+ assertSameOrigin(request);
2506
+ if (typeof body?.email !== "string" || typeof body?.password !== "string") {
2507
+ throw new InvalidQueryError("Signing in requires an email and a password.");
2508
+ }
2509
+ const account = await runtime.signIn(body.email, body.password, attemptKey(request, body.email));
2510
+ if (!account) {
2511
+ throw new UnauthorizedError("Those details do not match an account.");
2512
+ }
2513
+ setSessionCookie(response, signSession(account.id, runtime.secret, runtime.maxAge), runtime, request);
2514
+ return success({
2515
+ account: summarise(account)
2516
+ });
2517
+ }
2518
+ /**
2519
+ * `POST /admin/auth/logout`.
2520
+ *
2521
+ * A POST rather than a GET, so a link or an image somewhere else cannot sign
2522
+ * someone out by being loaded. Always `200`: signing out when you were not
2523
+ * signed in is not a failure, it is the state you asked for.
2524
+ */
2525
+ logout(request, response) {
2526
+ const runtime = this.runtime();
2527
+ assertSameOrigin(request);
2528
+ clearSessionCookie(response, runtime, request);
2529
+ return success({
2530
+ account: null
2531
+ });
2532
+ }
2533
+ /**
2534
+ * The built-in auth's runtime, or a 404.
2535
+ *
2536
+ * Not a 500: an application using its own `AdminAuth` has no login routes,
2537
+ * and "this endpoint does not exist here" is exactly true.
2538
+ */
2539
+ runtime() {
2540
+ const runtime = builtInRuntimeOf(this.auth);
2541
+ if (!runtime) {
2542
+ throw new import_common8.NotFoundException("This admin is not using the built-in authentication, so it has no login routes.");
2543
+ }
2544
+ return runtime;
2545
+ }
2546
+ };
2547
+ _ts_decorate5([
2548
+ (0, import_common8.Get)("session"),
2549
+ _ts_param4(0, (0, import_common8.Req)()),
2550
+ _ts_metadata4("design:type", Function),
2551
+ _ts_metadata4("design:paramtypes", [
2552
+ typeof RawRequest === "undefined" ? Object : RawRequest
2553
+ ]),
2554
+ _ts_metadata4("design:returntype", Promise)
2555
+ ], AdminAuthController.prototype, "session", null);
2556
+ _ts_decorate5([
2557
+ (0, import_common8.Post)("login"),
2558
+ (0, import_common8.HttpCode)(200),
2559
+ _ts_param4(0, (0, import_common8.Req)()),
2560
+ _ts_param4(1, (0, import_common8.Res)({
2561
+ passthrough: true
2562
+ })),
2563
+ _ts_param4(2, (0, import_common8.Body)()),
2564
+ _ts_metadata4("design:type", Function),
2565
+ _ts_metadata4("design:paramtypes", [
2566
+ typeof RawRequest === "undefined" ? Object : RawRequest,
2567
+ Object,
2568
+ Object
2569
+ ]),
2570
+ _ts_metadata4("design:returntype", Promise)
2571
+ ], AdminAuthController.prototype, "login", null);
2572
+ _ts_decorate5([
2573
+ (0, import_common8.Post)("logout"),
2574
+ (0, import_common8.HttpCode)(200),
2575
+ _ts_param4(0, (0, import_common8.Req)()),
2576
+ _ts_param4(1, (0, import_common8.Res)({
2577
+ passthrough: true
2578
+ })),
2579
+ _ts_metadata4("design:type", Function),
2580
+ _ts_metadata4("design:paramtypes", [
2581
+ typeof RawRequest === "undefined" ? Object : RawRequest,
2582
+ Object
2583
+ ]),
2584
+ _ts_metadata4("design:returntype", typeof SuccessResponse === "undefined" ? Object : SuccessResponse)
2585
+ ], AdminAuthController.prototype, "logout", null);
2586
+ AdminAuthController = _ts_decorate5([
2587
+ (0, import_common8.Controller)("auth"),
2588
+ (0, import_common8.UseFilters)(AdminExceptionFilter),
2589
+ _ts_param4(0, (0, import_common8.Inject)(ADMIN_AUTH)),
2590
+ _ts_metadata4("design:type", Function),
2591
+ _ts_metadata4("design:paramtypes", [
2592
+ typeof AdminAuth === "undefined" ? Object : AdminAuth
2593
+ ])
2594
+ ], AdminAuthController);
2595
+ function assertSameOrigin(request) {
2596
+ const origin = header(request, "origin");
2597
+ if (origin === void 0) return;
2598
+ const host = header(request, "host");
2599
+ if (host === void 0) return;
2600
+ let sent;
2601
+ try {
2602
+ sent = new URL(origin).host;
2603
+ } catch {
2604
+ throw new UnauthorizedError("This request did not come from the admin.");
2605
+ }
2606
+ if (sent !== host) {
2607
+ throw new UnauthorizedError("This request did not come from the admin.");
2608
+ }
2609
+ }
2610
+ __name(assertSameOrigin, "assertSameOrigin");
2611
+ function header(request, name) {
2612
+ const value = request?.headers?.[name];
2613
+ return typeof value === "string" ? value : void 0;
2614
+ }
2615
+ __name(header, "header");
2616
+
2617
+ // src/auth/contract.ts
2618
+ var import_common9 = require("@nestjs/common");
2619
+ var logger3 = new import_common9.Logger("NestAdmin");
2620
+ function unsafeAllowAllRequests() {
2621
+ const auth = {
2622
+ authorize() {
2623
+ }
2624
+ };
2625
+ unsafeInstances.add(auth);
2626
+ return auth;
2627
+ }
2628
+ __name(unsafeAllowAllRequests, "unsafeAllowAllRequests");
2629
+ var unsafeInstances = /* @__PURE__ */ new WeakSet();
2630
+ function warnIfUnsafe(auth) {
2631
+ if (unsafeInstances.has(auth)) {
2632
+ logger3.warn("AdminModule is running with unsafeAllowAllRequests(): every admin route, including /admin/meta, is public. Do not deploy this.");
2633
+ }
2634
+ }
2635
+ __name(warnIfUnsafe, "warnIfUnsafe");
2636
+
2637
+ // src/auth/resource.ts
2638
+ function allowAllResources() {
2639
+ return {
2640
+ authorize() {
2641
+ return true;
2642
+ }
2643
+ };
2644
+ }
2645
+ __name(allowAllResources, "allowAllResources");
2646
+
2647
+ // src/mount-path.ts
2648
+ var DEFAULT_MOUNT_PATH = "/admin";
2649
+ function normaliseMountPath(path) {
2650
+ if (path === void 0) return DEFAULT_MOUNT_PATH;
2651
+ if (typeof path !== "string") {
2652
+ throw new TypeError(`AdminModule \`path\` must be a string, received ${typeof path}.`);
2653
+ }
2654
+ const segments = path.split("/").filter((segment) => segment.length > 0);
2655
+ if (segments.length === 0) {
2656
+ throw new Error('AdminModule `path` cannot be empty or "/". The admin routes end in `:model`, so mounting them at the root would capture every unmatched request in the application. Choose a path such as "/admin".');
2657
+ }
2658
+ for (const segment of segments) {
2659
+ if (!/^[A-Za-z0-9._~-]+$/.test(segment)) {
2660
+ throw new Error(`AdminModule \`path\` segment "${segment}" is not a plain path segment. Use letters, digits, and any of . _ ~ - : the admin builds both URLs and HTML from this value, so it cannot contain patterns or markup.`);
2661
+ }
2662
+ }
2663
+ return `/${segments.join("/")}`;
2664
+ }
2665
+ __name(normaliseMountPath, "normaliseMountPath");
2666
+
2667
+ // src/ui/assets.ts
2668
+ var import_node_fs = require("fs");
2669
+ var import_node_path = require("path");
2670
+ var import_node_url = require("url");
2671
+
2672
+ // src/ui/colour.ts
2673
+ var READABLE = 4.5;
2674
+ var VISIBLE = 3;
2675
+ function channels(hex) {
2676
+ const value = hex.length === 4 ? hex.slice(1).split("").map((part) => part + part).join("") : hex.slice(1);
2677
+ return [
2678
+ Number.parseInt(value.slice(0, 2), 16) / 255,
2679
+ Number.parseInt(value.slice(2, 4), 16) / 255,
2680
+ Number.parseInt(value.slice(4, 6), 16) / 255
2681
+ ];
2682
+ }
2683
+ __name(channels, "channels");
2684
+ function toHex([r, g, b]) {
2685
+ const part = /* @__PURE__ */ __name((value) => Math.round(Math.min(1, Math.max(0, value)) * 255).toString(16).padStart(2, "0"), "part");
2686
+ return `#${part(r)}${part(g)}${part(b)}`;
2687
+ }
2688
+ __name(toHex, "toHex");
2689
+ function luminance(rgb) {
2690
+ const [r, g, b] = rgb.map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
2691
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
2692
+ }
2693
+ __name(luminance, "luminance");
2694
+ function contrast(a, b) {
2695
+ const [lighter, darker] = [
2696
+ luminance(a),
2697
+ luminance(b)
2698
+ ].sort((x, y) => y - x);
2699
+ return (lighter + 0.05) / (darker + 0.05);
2700
+ }
2701
+ __name(contrast, "contrast");
2702
+ var LIGHT_INK = [
2703
+ 0.985,
2704
+ 0.985,
2705
+ 0.99
2706
+ ];
2707
+ var DARK_INK = [
2708
+ 0.09,
2709
+ 0.1,
2710
+ 0.12
2711
+ ];
2712
+ var LIGHT_PAGE = [
2713
+ 0.976,
2714
+ 0.98,
2715
+ 0.984
2716
+ ];
2717
+ var DARK_PAGE = [
2718
+ 0.09,
2719
+ 0.1,
2720
+ 0.12
2721
+ ];
2722
+ function readableInk(hex) {
2723
+ const brand = channels(hex);
2724
+ return contrast(brand, LIGHT_INK) >= contrast(brand, DARK_INK) ? toHex(LIGHT_INK) : toHex(DARK_INK);
2725
+ }
2726
+ __name(readableInk, "readableInk");
2727
+ function mix(rgb, towards, amount) {
2728
+ return rgb.map((channel) => channel + (towards - channel) * amount);
2729
+ }
2730
+ __name(mix, "mix");
2731
+ function visibleOn(hex, page, role = "text") {
2732
+ const background = page === "dark" ? DARK_PAGE : LIGHT_PAGE;
2733
+ const towards = page === "dark" ? 1 : 0;
2734
+ const floor = role === "fill" ? VISIBLE : READABLE;
2735
+ let colour = channels(hex);
2736
+ for (let step = 0; step < 16; step++) {
2737
+ if (contrast(colour, background) >= floor) break;
2738
+ colour = mix(colour, towards, 0.05);
2739
+ }
2740
+ return toHex(colour);
2741
+ }
2742
+ __name(visibleOn, "visibleOn");
2743
+
2744
+ // src/ui/theme.ts
2745
+ var HEX_COLOUR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
2746
+ var SAFE_TEXT = /^[^<>&"'`\\]{1,64}$/;
2747
+ var SAFE_URL = /^(?:https?:\/\/[^\s<>"'`\\]+|data:image\/[a-z+]+;base64,[A-Za-z0-9+/=]+)$/;
2748
+ var APPEARANCES = /* @__PURE__ */ new Set([
2749
+ "system",
2750
+ "light",
2751
+ "dark"
2752
+ ]);
2753
+ var THEME_KEYS = /* @__PURE__ */ new Set([
2754
+ "brandColor",
2755
+ "title",
2756
+ "logoUrl",
2757
+ "appearance"
2758
+ ]);
2759
+ function assertUsableTheme(theme) {
2760
+ if (!theme) return;
2761
+ if (theme.brandColor !== void 0 && !HEX_COLOUR.test(theme.brandColor)) {
2762
+ throw new Error(`AdminModule \`theme.brandColor\` must be a hex colour such as "#0b6e6e", received ${JSON.stringify(theme.brandColor)}.`);
2763
+ }
2764
+ if (theme.title !== void 0 && !SAFE_TEXT.test(theme.title)) {
2765
+ throw new Error(`AdminModule \`theme.title\` must be plain text of at most 64 characters, without < > & " ' \` or backslashes. It is written into the served page.`);
2766
+ }
2767
+ if (theme.logoUrl !== void 0 && !SAFE_URL.test(theme.logoUrl)) {
2768
+ throw new Error(`AdminModule \`theme.logoUrl\` must be an http(s) URL or a data:image URI, received ${JSON.stringify(theme.logoUrl)}.`);
2769
+ }
2770
+ if (theme.appearance !== void 0 && !APPEARANCES.has(theme.appearance)) {
2771
+ throw new Error(`AdminModule \`theme.appearance\` must be "system", "light" or "dark", received ${JSON.stringify(theme.appearance)}.`);
2772
+ }
2773
+ const unknown = Object.keys(theme).filter((key) => !THEME_KEYS.has(key));
2774
+ if (unknown.length > 0) {
2775
+ throw new Error(`AdminModule \`theme\` has no option${unknown.length === 1 ? "" : "s"} called ${unknown.join(", ")}. Known options: ${[
2776
+ ...THEME_KEYS
2777
+ ].join(", ")}.`);
2778
+ }
2779
+ }
2780
+ __name(assertUsableTheme, "assertUsableTheme");
2781
+ function renderTheme(theme) {
2782
+ if (!theme || !theme.brandColor && !theme.title && !theme.logoUrl && !theme.appearance) {
2783
+ return "";
2784
+ }
2785
+ const style = theme.brandColor ? `<style>${brandRules(theme.brandColor)}</style>` : "";
2786
+ const globals = JSON.stringify({
2787
+ ...theme.title !== void 0 ? {
2788
+ title: theme.title
2789
+ } : {},
2790
+ ...theme.logoUrl !== void 0 ? {
2791
+ logoUrl: theme.logoUrl
2792
+ } : {},
2793
+ ...theme.appearance !== void 0 ? {
2794
+ appearance: theme.appearance
2795
+ } : {}
2796
+ });
2797
+ return `${style}<script>window.__NEST_ADMIN_THEME__ = ${globals}</script>`;
2798
+ }
2799
+ __name(renderTheme, "renderTheme");
2800
+ function brandRules(brand) {
2801
+ const rules = /* @__PURE__ */ __name((page) => {
2802
+ const fill = visibleOn(brand, page, "fill");
2803
+ const text = visibleOn(brand, page, "text");
2804
+ return `--primary:${fill};--primary-foreground:${readableInk(fill)};--link:${text}`;
2805
+ }, "rules");
2806
+ return `:root{${rules("light")}}.dark{${rules("dark")}}`;
2807
+ }
2808
+ __name(brandRules, "brandRules");
2809
+
2810
+ // src/ui/assets.ts
2811
+ function moduleDirectory() {
2812
+ return (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
2813
+ }
2814
+ __name(moduleDirectory, "moduleDirectory");
2815
+ function uiRoot() {
2816
+ return (0, import_node_path.join)(moduleDirectory(), "admin-ui");
2817
+ }
2818
+ __name(uiRoot, "uiRoot");
2819
+ function uiAvailable(root = uiRoot()) {
2820
+ return (0, import_node_fs.existsSync)((0, import_node_path.join)(root, "index.html"));
2821
+ }
2822
+ __name(uiAvailable, "uiAvailable");
2823
+ var CONTENT_TYPES = {
2824
+ ".html": "text/html; charset=utf-8",
2825
+ ".js": "text/javascript; charset=utf-8",
2826
+ ".mjs": "text/javascript; charset=utf-8",
2827
+ ".css": "text/css; charset=utf-8",
2828
+ ".map": "application/json; charset=utf-8",
2829
+ ".json": "application/json; charset=utf-8",
2830
+ ".svg": "image/svg+xml",
2831
+ ".png": "image/png",
2832
+ ".jpg": "image/jpeg",
2833
+ ".jpeg": "image/jpeg",
2834
+ ".gif": "image/gif",
2835
+ ".ico": "image/x-icon",
2836
+ ".webp": "image/webp",
2837
+ ".woff": "font/woff",
2838
+ ".woff2": "font/woff2",
2839
+ ".ttf": "font/ttf"
2840
+ };
2841
+ function contentTypeFor(fileName) {
2842
+ const dot = fileName.lastIndexOf(".");
2843
+ const extension = dot === -1 ? "" : fileName.slice(dot).toLowerCase();
2844
+ return CONTENT_TYPES[extension] ?? "application/octet-stream";
2845
+ }
2846
+ __name(contentTypeFor, "contentTypeFor");
2847
+ function readAsset(fileName, root = uiRoot()) {
2848
+ if (!/^[\w.-]+$/.test(fileName) || fileName.includes("..")) return void 0;
2849
+ const assetsDirectory = (0, import_node_path.join)(root, "assets");
2850
+ const candidate = (0, import_node_path.resolve)(assetsDirectory, fileName);
2851
+ if (!candidate.startsWith(assetsDirectory + import_node_path.sep)) return void 0;
2852
+ if (!(0, import_node_fs.existsSync)(candidate) || !(0, import_node_fs.statSync)(candidate).isFile()) return void 0;
2853
+ return (0, import_node_fs.readFileSync)(candidate);
2854
+ }
2855
+ __name(readAsset, "readAsset");
2856
+ function readIndexHtml(root = uiRoot()) {
2857
+ const indexPath = (0, import_node_path.join)(root, "index.html");
2858
+ return (0, import_node_fs.existsSync)(indexPath) ? (0, import_node_fs.readFileSync)(indexPath) : void 0;
2859
+ }
2860
+ __name(readIndexHtml, "readIndexHtml");
2861
+ var UI_BASE_PLACEHOLDER = "/__nest-admin-base__";
2862
+ function renderShell(mountPath, root = uiRoot(), theme) {
2863
+ const shell = readIndexHtml(root);
2864
+ if (!shell) return void 0;
2865
+ const injected = ` <script>window.__NEST_ADMIN_BASE__ = "${mountPath}"</script>
2866
+ ${renderTheme(theme)}
2867
+ </head>`;
2868
+ let html = shell.toString("utf8").split(`${UI_BASE_PLACEHOLDER}/`).join(`${mountPath}/`).replace("</head>", injected);
2869
+ if (theme?.title !== void 0) {
2870
+ html = html.replace(/<title>[^<]*<\/title>/, `<title>${theme.title}</title>`);
2871
+ }
2872
+ return Buffer.from(html, "utf8");
2873
+ }
2874
+ __name(renderShell, "renderShell");
2875
+
2876
+ // src/ui/controller.ts
2877
+ var import_common10 = require("@nestjs/common");
2878
+ function _ts_decorate6(decorators, target, key, desc) {
2879
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2880
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2881
+ r = Reflect.decorate(decorators, target, key, desc);
2882
+ } else {
2883
+ for (var i = decorators.length - 1; i >= 0; i--) {
2884
+ if (d = decorators[i]) {
2885
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2886
+ }
2887
+ }
2888
+ }
2889
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2890
+ }
2891
+ __name(_ts_decorate6, "_ts_decorate");
2892
+ function _ts_metadata5(metadataKey, metadataValue) {
2893
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
2894
+ return Reflect.metadata(metadataKey, metadataValue);
2895
+ }
2896
+ }
2897
+ __name(_ts_metadata5, "_ts_metadata");
2898
+ function _ts_param5(paramIndex, decorator) {
2899
+ return function(target, key) {
2900
+ decorator(target, key, paramIndex);
2901
+ };
2902
+ }
2903
+ __name(_ts_param5, "_ts_param");
2904
+ var AdminUiController = class {
2905
+ static {
2906
+ __name(this, "AdminUiController");
2907
+ }
2908
+ root;
2909
+ mountPath;
2910
+ theme;
2911
+ /**
2912
+ * The rendered shell, built on first use.
2913
+ *
2914
+ * It depends only on the bundled file and the mount path, and neither changes
2915
+ * while the application runs. Held on the instance rather than in a
2916
+ * module-level cache: the package ships two bundles that each inline their
2917
+ * own copy of a module, so module-level state is not shared between them.
2918
+ */
2919
+ shell;
2920
+ constructor(root, mountPath, theme) {
2921
+ this.root = root;
2922
+ this.mountPath = mountPath;
2923
+ this.theme = theme;
2924
+ }
2925
+ /**
2926
+ * `GET /admin` - the SPA shell.
2927
+ *
2928
+ * `no-cache` rather than a long max-age: the HTML names hashed asset files,
2929
+ * so a cached shell would keep pointing at a previous deployment's bundle.
2930
+ */
2931
+ index() {
2932
+ this.shell ??= renderShell(this.mountPath, this.root, this.theme);
2933
+ const html = this.shell;
2934
+ if (!html) {
2935
+ throw new import_common10.NotFoundException("The admin UI was not bundled with this package. This build is missing dist/admin-ui - see docs/publishing.md.");
2936
+ }
2937
+ return new import_common10.StreamableFile(html, {
2938
+ type: "text/html; charset=utf-8"
2939
+ });
2940
+ }
2941
+ /**
2942
+ * `GET /admin/assets/:file`
2943
+ *
2944
+ * A single path segment, so a nested path cannot be requested at all; the
2945
+ * reader in `assets.ts` re-checks the name and the resolved location anyway.
2946
+ *
2947
+ * Vite emits content-hashed filenames, so these are immutable and safe to
2948
+ * cache for a long time.
2949
+ */
2950
+ asset(file) {
2951
+ const contents = readAsset(file, this.root);
2952
+ if (!contents) throw new import_common10.NotFoundException(`No admin UI asset named "${file}".`);
2953
+ return new import_common10.StreamableFile(contents, {
2954
+ type: contentTypeFor(file)
2955
+ });
2956
+ }
2957
+ };
2958
+ _ts_decorate6([
2959
+ (0, import_common10.Get)(),
2960
+ (0, import_common10.Header)("Cache-Control", "no-cache"),
2961
+ _ts_metadata5("design:type", Function),
2962
+ _ts_metadata5("design:paramtypes", []),
2963
+ _ts_metadata5("design:returntype", typeof import_common10.StreamableFile === "undefined" ? Object : import_common10.StreamableFile)
2964
+ ], AdminUiController.prototype, "index", null);
2965
+ _ts_decorate6([
2966
+ (0, import_common10.Get)("assets/:file"),
2967
+ (0, import_common10.Header)("Cache-Control", "public, max-age=31536000, immutable"),
2968
+ _ts_param5(0, (0, import_common10.Param)("file")),
2969
+ _ts_metadata5("design:type", Function),
2970
+ _ts_metadata5("design:paramtypes", [
2971
+ String
2972
+ ]),
2973
+ _ts_metadata5("design:returntype", typeof import_common10.StreamableFile === "undefined" ? Object : import_common10.StreamableFile)
2974
+ ], AdminUiController.prototype, "asset", null);
2975
+ AdminUiController = _ts_decorate6([
2976
+ (0, import_common10.Controller)(),
2977
+ _ts_param5(0, (0, import_common10.Inject)(ADMIN_UI_ROOT)),
2978
+ _ts_param5(1, (0, import_common10.Inject)(ADMIN_MOUNT_PATH)),
2979
+ _ts_param5(2, (0, import_common10.Inject)(ADMIN_THEME)),
2980
+ _ts_metadata5("design:type", Function),
2981
+ _ts_metadata5("design:paramtypes", [
2982
+ String,
2983
+ String,
2984
+ Object
2985
+ ])
2986
+ ], AdminUiController);
2987
+
2988
+ // src/module.ts
2989
+ function _ts_decorate7(decorators, target, key, desc) {
2990
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2991
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2992
+ r = Reflect.decorate(decorators, target, key, desc);
2993
+ } else {
2994
+ for (var i = decorators.length - 1; i >= 0; i--) {
2995
+ if (d = decorators[i]) {
2996
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2997
+ }
2998
+ }
2999
+ }
3000
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3001
+ }
3002
+ __name(_ts_decorate7, "_ts_decorate");
3003
+ function assertUsableOptions(options, caller) {
3004
+ if (!options?.adapter) {
3005
+ throw new Error(`AdminModule.${caller}() requires an \`adapter\`. Construct one in your application, for example \`new PrismaAdapter({ client: prisma })\`.`);
3006
+ }
3007
+ if (!options.auth || typeof options.auth.authorize !== "function") {
3008
+ throw new Error(`AdminModule.${caller}() requires an \`auth\` implementation with an \`authorize(context)\` method. The admin API exposes every record and the whole schema, so it is never public by default. For local development only, pass \`unsafeAllowAllRequests()\`.`);
3009
+ }
3010
+ if (options.resourceAuth && typeof options.resourceAuth.authorize !== "function") {
3011
+ throw new Error(`AdminModule.${caller}() was given a \`resourceAuth\` without an \`authorize(resource)\` method. Omit it to allow every model, or supply an implementation.`);
3012
+ }
3013
+ warnIfUnsafe(options.auth);
3014
+ }
3015
+ __name(assertUsableOptions, "assertUsableOptions");
3016
+ function warnIfUiMissing(resolvedUiRoot, mountPath) {
3017
+ if (uiAvailable(resolvedUiRoot)) return;
3018
+ new import_common11.Logger("NestAdmin").warn(`The admin UI was not found in this build; ${mountPath} will return 404. The API under ${mountPath} is unaffected.`);
3019
+ }
3020
+ __name(warnIfUiMissing, "warnIfUiMissing");
3021
+ function defineModule(mountPath, resolvedUiRoot, theme, optionProviders, extraImports = []) {
3022
+ return {
3023
+ module: AdminModule,
3024
+ imports: [
3025
+ ...extraImports,
3026
+ // The mount path is applied here, not on the controllers: `@Controller()`
3027
+ // is evaluated when the class is defined, long before either entry point
3028
+ // sees any options. `RouterModule` prefixes the module's routes and
3029
+ // preserves controller order, which the collision rule below depends on.
3030
+ import_core10.RouterModule.register([
3031
+ {
3032
+ path: mountPath,
3033
+ module: AdminModule
3034
+ }
3035
+ ])
3036
+ ],
3037
+ // Order matters and is the whole answer to the route collision. The UI
3038
+ // controller binds exactly two paths - the mount path itself and
3039
+ // `assets/:file` - and is matched first, so `assets` can never be read as a
3040
+ // model name. The auth controller claims `auth/*` next, for the same
3041
+ // reason and at the same cost: a model named `auth` is unreachable, as one
3042
+ // named `assets` or `actions` already was. Everything else falls through to
3043
+ // the API controller.
3044
+ controllers: [
3045
+ AdminUiController,
3046
+ AdminAuthController,
3047
+ AdminController
3048
+ ],
3049
+ providers: [
3050
+ ...optionProviders,
3051
+ {
3052
+ provide: ADMIN_UI_ROOT,
3053
+ useValue: resolvedUiRoot
3054
+ },
3055
+ {
3056
+ provide: ADMIN_MOUNT_PATH,
3057
+ useValue: mountPath
3058
+ },
3059
+ {
3060
+ provide: ADMIN_THEME,
3061
+ useValue: theme
3062
+ },
3063
+ AdminService,
3064
+ // Provided so Nest can resolve them for `@UseGuards` / `@UseFilters` on
3065
+ // the controller. Deliberately not APP_GUARD or APP_FILTER: either would
3066
+ // take over behaviour for the whole host application rather than just the
3067
+ // admin routes.
3068
+ AdminAuthGuard,
3069
+ AdminExceptionFilter
3070
+ ],
3071
+ exports: [
3072
+ AdminService
3073
+ ]
3074
+ };
3075
+ }
3076
+ __name(defineModule, "defineModule");
3077
+ var STRUCTURAL_OPTIONS = [
3078
+ "path",
3079
+ "uiRoot",
3080
+ "theme"
3081
+ ];
3082
+ function assertNoStructuralOptions(resolved) {
3083
+ const misplaced = STRUCTURAL_OPTIONS.filter((key) => key in resolved);
3084
+ if (misplaced.length === 0) return;
3085
+ const one = misplaced.length === 1;
3086
+ throw new Error(`AdminModule.forRootAsync() received ${misplaced.join(", ")} from its factory. ${one ? "That option is" : "Those options are"} structural: routes are registered before any provider exists, so ${one ? "it" : "they"} must be passed to forRootAsync() itself, beside \`imports\` and \`inject\`, rather than returned from \`useFactory\`.`);
3087
+ }
3088
+ __name(assertNoStructuralOptions, "assertNoStructuralOptions");
3089
+ function optionsProviders(options) {
3090
+ const validate = /* @__PURE__ */ __name((resolved) => {
3091
+ assertNoStructuralOptions(resolved);
3092
+ assertUsableOptions(resolved, "forRootAsync");
3093
+ return resolved;
3094
+ }, "validate");
3095
+ if (options.useFactory) {
3096
+ return [
3097
+ {
3098
+ provide: ADMIN_OPTIONS,
3099
+ useFactory: /* @__PURE__ */ __name(async (...args) => validate(await options.useFactory(...args)), "useFactory"),
3100
+ inject: options.inject ?? []
3101
+ }
3102
+ ];
3103
+ }
3104
+ const factoryClass = options.useExisting ?? options.useClass;
3105
+ return [
3106
+ // `useClass` has to be instantiated by Nest before it can be asked;
3107
+ // `useExisting` is already provided by the application.
3108
+ ...options.useClass ? [
3109
+ {
3110
+ provide: options.useClass,
3111
+ useClass: options.useClass
3112
+ }
3113
+ ] : [],
3114
+ {
3115
+ provide: ADMIN_OPTIONS,
3116
+ useFactory: /* @__PURE__ */ __name(async (factory) => validate(await factory.createAdminOptions()), "useFactory"),
3117
+ inject: [
3118
+ factoryClass
3119
+ ]
3120
+ }
3121
+ ];
3122
+ }
3123
+ __name(optionsProviders, "optionsProviders");
3124
+ var AdminModule = class {
3125
+ static {
3126
+ __name(this, "AdminModule");
3127
+ }
3128
+ static forRoot(options) {
3129
+ assertUsableOptions(options, "forRoot");
3130
+ const mountPath = normaliseMountPath(options.path);
3131
+ const resolvedUiRoot = options.uiRoot ?? uiRoot();
3132
+ warnIfUiMissing(resolvedUiRoot, mountPath);
3133
+ assertUsableTheme(options.theme);
3134
+ return defineModule(mountPath, resolvedUiRoot, options.theme, [
3135
+ {
3136
+ provide: ADMIN_ADAPTER,
3137
+ useValue: options.adapter
3138
+ },
3139
+ {
3140
+ provide: ADMIN_RESOURCES,
3141
+ useValue: options.resources
3142
+ },
3143
+ {
3144
+ provide: ADMIN_MODELS,
3145
+ useValue: options.models
3146
+ },
3147
+ {
3148
+ provide: ADMIN_HOOKS,
3149
+ useValue: options.hooks
3150
+ },
3151
+ {
3152
+ provide: ADMIN_ACTIONS,
3153
+ useValue: options.actions
3154
+ },
3155
+ {
3156
+ provide: ADMIN_DASHBOARD,
3157
+ useValue: options.dashboard
3158
+ },
3159
+ {
3160
+ provide: ADMIN_AUTH,
3161
+ useValue: options.auth
3162
+ },
3163
+ // Always provided, so injection resolves whether or not the consumer
3164
+ // supplied a policy. The default permits every model.
3165
+ {
3166
+ provide: ADMIN_RESOURCE_AUTH,
3167
+ useValue: options.resourceAuth ?? allowAllResources()
3168
+ }
3169
+ ]);
3170
+ }
3171
+ /**
3172
+ * The same module, with the adapter and the auth policy resolved through DI.
3173
+ *
3174
+ * For the ordinary case where those things are not available when the module
3175
+ * is declared: a `PrismaService` that belongs to another module, a connection
3176
+ * string that comes from `ConfigService`.
3177
+ *
3178
+ * ```ts
3179
+ * AdminModule.forRootAsync({
3180
+ * imports: [PrismaModule, ConfigModule],
3181
+ * inject: [PrismaService, ConfigService],
3182
+ * useFactory: (prisma: PrismaService, config: ConfigService) => ({
3183
+ * adapter: new PrismaAdapter({ client: prisma }),
3184
+ * auth: new SessionAdminAuth(config.get('ADMIN_ROLE')),
3185
+ * }),
3186
+ * })
3187
+ * ```
3188
+ *
3189
+ * `path` stays on this object rather than coming from the factory. Routes are
3190
+ * registered when the module is defined, which is before any provider has
3191
+ * been instantiated, so the mount path cannot wait for an injection - and a
3192
+ * `path` returned from the factory would be silently ignored, which is worse
3193
+ * than not offering it.
3194
+ */
3195
+ static forRootAsync(options) {
3196
+ if (!options?.useFactory && !options?.useClass && !options?.useExisting) {
3197
+ throw new Error("AdminModule.forRootAsync() requires one of `useFactory`, `useClass` or `useExisting`. To configure the admin directly, use forRoot().");
3198
+ }
3199
+ const mountPath = normaliseMountPath(options.path);
3200
+ const resolvedUiRoot = options.uiRoot ?? uiRoot();
3201
+ warnIfUiMissing(resolvedUiRoot, mountPath);
3202
+ assertUsableTheme(options.theme);
3203
+ const derive2 = /* @__PURE__ */ __name((token, read) => ({
3204
+ provide: token,
3205
+ useFactory: read,
3206
+ inject: [
3207
+ ADMIN_OPTIONS
3208
+ ]
3209
+ }), "derive");
3210
+ return defineModule(mountPath, resolvedUiRoot, options.theme, [
3211
+ ...optionsProviders(options),
3212
+ derive2(ADMIN_ADAPTER, (resolved) => resolved.adapter),
3213
+ derive2(ADMIN_RESOURCES, (resolved) => resolved.resources),
3214
+ derive2(ADMIN_MODELS, (resolved) => resolved.models),
3215
+ derive2(ADMIN_HOOKS, (resolved) => resolved.hooks),
3216
+ derive2(ADMIN_ACTIONS, (resolved) => resolved.actions),
3217
+ derive2(ADMIN_DASHBOARD, (resolved) => resolved.dashboard),
3218
+ derive2(ADMIN_AUTH, (resolved) => resolved.auth),
3219
+ derive2(ADMIN_RESOURCE_AUTH, (resolved) => resolved.resourceAuth ?? allowAllResources())
3220
+ ], options.imports ?? []);
3221
+ }
3222
+ };
3223
+ AdminModule = _ts_decorate7([
3224
+ (0, import_common11.Module)({})
3225
+ ], AdminModule);
3226
+ // Annotate the CommonJS export names for ESM import in node:
3227
+ 0 && (module.exports = {
3228
+ AdapterError,
3229
+ AdminModule,
3230
+ ConstraintError,
3231
+ FieldNotFoundError,
3232
+ ForbiddenError,
3233
+ InvalidQueryError,
3234
+ ModelNotFoundError,
3235
+ NestAdminError,
3236
+ RecordNotFoundError,
3237
+ UnauthorizedError,
3238
+ ValidationError,
3239
+ adminAccountOf,
3240
+ builtInAuth,
3241
+ generateSessionSecret,
3242
+ hashAdminPassword,
3243
+ isNestAdminError,
3244
+ unsafeAllowAllRequests,
3245
+ verifyAdminPassword
3246
+ });
3247
+ //# sourceMappingURL=index.cjs.map