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