ad2app-lib 1.44.0 → 1.44.1
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/types/agency.d.ts +29 -11
- package/dist/types/agency.js +131 -32
- package/package.json +2 -2
- package/src/types/agency.test.ts +58 -0
- package/src/types/agency.ts +105 -29
package/dist/types/agency.d.ts
CHANGED
|
@@ -203,25 +203,40 @@ export interface I_SponsoredBy {
|
|
|
203
203
|
/** Present once the sponsorship is ending: the date their plan runs to. */
|
|
204
204
|
until?: string;
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Every DTO below carries class-validator decorators and a constructor that
|
|
208
|
+
* survives being called with nothing.
|
|
209
|
+
*
|
|
210
|
+
* Both are load-bearing and were missing until 2026-09-22. The backend mounts
|
|
211
|
+
* a global `ValidationPipe`, which validates the decorated metadata of the
|
|
212
|
+
* class a handler's `@Body()` is typed with — an undecorated class, or a body
|
|
213
|
+
* typed as a plain interface, is simply passed through unchecked. And the pipe
|
|
214
|
+
* instantiates the class with NO arguments, so a constructor that reads
|
|
215
|
+
* `data.name` would throw a TypeError on every request rather than validate
|
|
216
|
+
* anything. The rest of this library's DTOs take `data?: Partial<T>` for that
|
|
217
|
+
* reason; these now do too.
|
|
218
|
+
*/
|
|
206
219
|
export declare class CreateAgencyOrganizationDto {
|
|
207
220
|
name: string;
|
|
221
|
+
/** Rendered as a link on the admin screen, so the protocol is checked here. */
|
|
208
222
|
website?: string;
|
|
209
223
|
registryId: string;
|
|
224
|
+
/** ISO-3166 alpha-2: which register a reviewer should look in. */
|
|
210
225
|
registryCountry: string;
|
|
211
226
|
contactEmail: string;
|
|
212
|
-
constructor(data
|
|
227
|
+
constructor(data?: Partial<CreateAgencyOrganizationDto>);
|
|
213
228
|
}
|
|
214
229
|
export declare class InviteAgencyMemberDto {
|
|
215
230
|
email: string;
|
|
216
|
-
constructor(data
|
|
231
|
+
constructor(data?: Partial<InviteAgencyMemberDto>);
|
|
217
232
|
}
|
|
218
233
|
export declare class RedeemAgencyInviteDto {
|
|
219
234
|
token: string;
|
|
220
|
-
constructor(data
|
|
235
|
+
constructor(data?: Partial<RedeemAgencyInviteDto>);
|
|
221
236
|
}
|
|
222
237
|
export declare class AcceptAgencyMemberInviteDto {
|
|
223
238
|
token: string;
|
|
224
|
-
constructor(data
|
|
239
|
+
constructor(data?: Partial<AcceptAgencyMemberInviteDto>);
|
|
225
240
|
}
|
|
226
241
|
/**
|
|
227
242
|
* Accepting consent. Either arm identifies the grant being answered; `scopes`
|
|
@@ -230,31 +245,34 @@ export declare class AcceptAgencyMemberInviteDto {
|
|
|
230
245
|
export declare class AcceptGrantDto {
|
|
231
246
|
token?: string;
|
|
232
247
|
grantId?: string;
|
|
248
|
+
/** A subset of the three reads. Never empty: sharing nothing is declining. */
|
|
233
249
|
scopes?: I_AgencyGrantScope[];
|
|
234
|
-
constructor(data
|
|
250
|
+
constructor(data?: Partial<AcceptGrantDto>);
|
|
235
251
|
}
|
|
236
252
|
/** The creator narrowing or widening a live grant — the only other write path. */
|
|
237
253
|
export declare class UpdateGrantScopesDto {
|
|
238
254
|
scopes: I_AgencyGrantScope[];
|
|
239
|
-
constructor(data
|
|
255
|
+
constructor(data?: Partial<UpdateGrantScopesDto>);
|
|
240
256
|
}
|
|
241
257
|
export declare class SponsorCreatorDto {
|
|
242
258
|
tier: SponsoredTier;
|
|
243
|
-
constructor(data
|
|
259
|
+
constructor(data?: Partial<SponsorCreatorDto>);
|
|
244
260
|
}
|
|
245
261
|
export declare class AgencyDiscoverQueryDto {
|
|
246
262
|
/** An EXACT platform username or e-mail. Never recorded in analytics (FR-015). */
|
|
247
263
|
q: string;
|
|
248
|
-
constructor(data
|
|
264
|
+
constructor(data?: Partial<AgencyDiscoverQueryDto>);
|
|
249
265
|
}
|
|
250
266
|
export declare class AgencyAccessRequestDto {
|
|
251
267
|
userId: string;
|
|
252
268
|
scopes: I_AgencyGrantScope[];
|
|
253
|
-
constructor(data
|
|
269
|
+
constructor(data?: Partial<AgencyAccessRequestDto>);
|
|
254
270
|
}
|
|
255
271
|
export declare class AdminSetOrganizationStatusDto {
|
|
256
272
|
status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
|
|
257
|
-
/** Required on reject — a rejected applicant is shown this (FR-002).
|
|
273
|
+
/** Required on reject — a rejected applicant is shown this (FR-002). The
|
|
274
|
+
* "required when rejecting" half is a rule about the PAIR, so it stays in
|
|
275
|
+
* the service where both fields are in hand. */
|
|
258
276
|
reason?: string;
|
|
259
|
-
constructor(data
|
|
277
|
+
constructor(data?: Partial<AdminSetOrganizationStatusDto>);
|
|
260
278
|
}
|
package/dist/types/agency.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* writes `grantedScopes` (FR-008, SC-002). Nothing here can express a write on
|
|
12
|
-
* a creator's account, and that is deliberate — the absence is the feature.
|
|
13
|
-
*
|
|
14
|
-
* See specs/175-agency-roster/contracts/lib-types.md.
|
|
15
|
-
*/
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
16
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
12
|
exports.AdminSetOrganizationStatusDto = exports.AgencyAccessRequestDto = exports.AgencyDiscoverQueryDto = exports.SponsorCreatorDto = exports.UpdateGrantScopesDto = exports.AcceptGrantDto = exports.AcceptAgencyMemberInviteDto = exports.RedeemAgencyInviteDto = exports.InviteAgencyMemberDto = exports.CreateAgencyOrganizationDto = exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = exports.AGENCY_ROSTER_ROW_STATES = exports.CREATOR_ACCESS_GRANT_STATES = exports.AGENCY_ORGANIZATION_STATUSES = exports.isAgencyGrantScope = exports.AGENCY_GRANT_SCOPES = exports.AGENCY_ROUTE_PATHS = void 0;
|
|
13
|
+
const class_validator_1 = require("class-validator");
|
|
18
14
|
// ── Cross-repo route constants ───────────────────────────────────────────────
|
|
19
15
|
/**
|
|
20
16
|
* Paths that BOTH repos have to agree on, with one home here.
|
|
@@ -125,76 +121,179 @@ exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
|
|
|
125
121
|
'stale',
|
|
126
122
|
];
|
|
127
123
|
// ── DTOs ─────────────────────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Every DTO below carries class-validator decorators and a constructor that
|
|
126
|
+
* survives being called with nothing.
|
|
127
|
+
*
|
|
128
|
+
* Both are load-bearing and were missing until 2026-09-22. The backend mounts
|
|
129
|
+
* a global `ValidationPipe`, which validates the decorated metadata of the
|
|
130
|
+
* class a handler's `@Body()` is typed with — an undecorated class, or a body
|
|
131
|
+
* typed as a plain interface, is simply passed through unchecked. And the pipe
|
|
132
|
+
* instantiates the class with NO arguments, so a constructor that reads
|
|
133
|
+
* `data.name` would throw a TypeError on every request rather than validate
|
|
134
|
+
* anything. The rest of this library's DTOs take `data?: Partial<T>` for that
|
|
135
|
+
* reason; these now do too.
|
|
136
|
+
*/
|
|
128
137
|
class CreateAgencyOrganizationDto {
|
|
129
138
|
constructor(data) {
|
|
130
|
-
this.name = data
|
|
131
|
-
this.website = data
|
|
132
|
-
this.registryId = data
|
|
133
|
-
this.registryCountry = data
|
|
134
|
-
this.contactEmail = data
|
|
139
|
+
this.name = data?.name ?? '';
|
|
140
|
+
this.website = data?.website;
|
|
141
|
+
this.registryId = data?.registryId ?? '';
|
|
142
|
+
this.registryCountry = data?.registryCountry ?? '';
|
|
143
|
+
this.contactEmail = data?.contactEmail ?? '';
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
exports.CreateAgencyOrganizationDto = CreateAgencyOrganizationDto;
|
|
147
|
+
__decorate([
|
|
148
|
+
(0, class_validator_1.IsString)(),
|
|
149
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
150
|
+
__metadata("design:type", String)
|
|
151
|
+
], CreateAgencyOrganizationDto.prototype, "name", void 0);
|
|
152
|
+
__decorate([
|
|
153
|
+
(0, class_validator_1.IsOptional)(),
|
|
154
|
+
(0, class_validator_1.IsUrl)({ protocols: ['http', 'https'], require_protocol: true }),
|
|
155
|
+
__metadata("design:type", String)
|
|
156
|
+
], CreateAgencyOrganizationDto.prototype, "website", void 0);
|
|
157
|
+
__decorate([
|
|
158
|
+
(0, class_validator_1.IsString)(),
|
|
159
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
160
|
+
__metadata("design:type", String)
|
|
161
|
+
], CreateAgencyOrganizationDto.prototype, "registryId", void 0);
|
|
162
|
+
__decorate([
|
|
163
|
+
(0, class_validator_1.IsString)(),
|
|
164
|
+
(0, class_validator_1.Length)(2, 2),
|
|
165
|
+
__metadata("design:type", String)
|
|
166
|
+
], CreateAgencyOrganizationDto.prototype, "registryCountry", void 0);
|
|
167
|
+
__decorate([
|
|
168
|
+
(0, class_validator_1.IsEmail)(),
|
|
169
|
+
__metadata("design:type", String)
|
|
170
|
+
], CreateAgencyOrganizationDto.prototype, "contactEmail", void 0);
|
|
138
171
|
class InviteAgencyMemberDto {
|
|
139
172
|
constructor(data) {
|
|
140
|
-
this.email = data
|
|
173
|
+
this.email = data?.email ?? '';
|
|
141
174
|
}
|
|
142
175
|
}
|
|
143
176
|
exports.InviteAgencyMemberDto = InviteAgencyMemberDto;
|
|
177
|
+
__decorate([
|
|
178
|
+
(0, class_validator_1.IsEmail)(),
|
|
179
|
+
__metadata("design:type", String)
|
|
180
|
+
], InviteAgencyMemberDto.prototype, "email", void 0);
|
|
144
181
|
class RedeemAgencyInviteDto {
|
|
145
182
|
constructor(data) {
|
|
146
|
-
this.token = data
|
|
183
|
+
this.token = data?.token ?? '';
|
|
147
184
|
}
|
|
148
185
|
}
|
|
149
186
|
exports.RedeemAgencyInviteDto = RedeemAgencyInviteDto;
|
|
187
|
+
__decorate([
|
|
188
|
+
(0, class_validator_1.IsString)(),
|
|
189
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
190
|
+
__metadata("design:type", String)
|
|
191
|
+
], RedeemAgencyInviteDto.prototype, "token", void 0);
|
|
150
192
|
class AcceptAgencyMemberInviteDto {
|
|
151
193
|
constructor(data) {
|
|
152
|
-
this.token = data
|
|
194
|
+
this.token = data?.token ?? '';
|
|
153
195
|
}
|
|
154
196
|
}
|
|
155
197
|
exports.AcceptAgencyMemberInviteDto = AcceptAgencyMemberInviteDto;
|
|
198
|
+
__decorate([
|
|
199
|
+
(0, class_validator_1.IsString)(),
|
|
200
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
201
|
+
__metadata("design:type", String)
|
|
202
|
+
], AcceptAgencyMemberInviteDto.prototype, "token", void 0);
|
|
156
203
|
/**
|
|
157
204
|
* Accepting consent. Either arm identifies the grant being answered; `scopes`
|
|
158
205
|
* is the creator's answer and defaults to the requested set when absent.
|
|
159
206
|
*/
|
|
160
207
|
class AcceptGrantDto {
|
|
161
208
|
constructor(data) {
|
|
162
|
-
this.token = data
|
|
163
|
-
this.grantId = data
|
|
164
|
-
this.scopes = data
|
|
209
|
+
this.token = data?.token;
|
|
210
|
+
this.grantId = data?.grantId;
|
|
211
|
+
this.scopes = data?.scopes;
|
|
165
212
|
}
|
|
166
213
|
}
|
|
167
214
|
exports.AcceptGrantDto = AcceptGrantDto;
|
|
215
|
+
__decorate([
|
|
216
|
+
(0, class_validator_1.IsOptional)(),
|
|
217
|
+
(0, class_validator_1.IsString)(),
|
|
218
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
219
|
+
__metadata("design:type", String)
|
|
220
|
+
], AcceptGrantDto.prototype, "token", void 0);
|
|
221
|
+
__decorate([
|
|
222
|
+
(0, class_validator_1.IsOptional)(),
|
|
223
|
+
(0, class_validator_1.IsUUID)(),
|
|
224
|
+
__metadata("design:type", String)
|
|
225
|
+
], AcceptGrantDto.prototype, "grantId", void 0);
|
|
226
|
+
__decorate([
|
|
227
|
+
(0, class_validator_1.IsOptional)(),
|
|
228
|
+
(0, class_validator_1.IsArray)(),
|
|
229
|
+
(0, class_validator_1.ArrayNotEmpty)(),
|
|
230
|
+
(0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
|
|
231
|
+
__metadata("design:type", Array)
|
|
232
|
+
], AcceptGrantDto.prototype, "scopes", void 0);
|
|
168
233
|
/** The creator narrowing or widening a live grant — the only other write path. */
|
|
169
234
|
class UpdateGrantScopesDto {
|
|
170
235
|
constructor(data) {
|
|
171
|
-
this.scopes = data
|
|
236
|
+
this.scopes = data?.scopes ?? [];
|
|
172
237
|
}
|
|
173
238
|
}
|
|
174
239
|
exports.UpdateGrantScopesDto = UpdateGrantScopesDto;
|
|
240
|
+
__decorate([
|
|
241
|
+
(0, class_validator_1.IsArray)(),
|
|
242
|
+
(0, class_validator_1.ArrayNotEmpty)(),
|
|
243
|
+
(0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
|
|
244
|
+
__metadata("design:type", Array)
|
|
245
|
+
], UpdateGrantScopesDto.prototype, "scopes", void 0);
|
|
175
246
|
class SponsorCreatorDto {
|
|
176
247
|
constructor(data) {
|
|
177
|
-
this.tier = data
|
|
248
|
+
this.tier = data?.tier ?? 'starter';
|
|
178
249
|
}
|
|
179
250
|
}
|
|
180
251
|
exports.SponsorCreatorDto = SponsorCreatorDto;
|
|
252
|
+
__decorate([
|
|
253
|
+
(0, class_validator_1.IsIn)(['starter', 'pro']),
|
|
254
|
+
__metadata("design:type", String)
|
|
255
|
+
], SponsorCreatorDto.prototype, "tier", void 0);
|
|
181
256
|
class AgencyDiscoverQueryDto {
|
|
182
257
|
constructor(data) {
|
|
183
|
-
this.q = data
|
|
258
|
+
this.q = data?.q ?? '';
|
|
184
259
|
}
|
|
185
260
|
}
|
|
186
261
|
exports.AgencyDiscoverQueryDto = AgencyDiscoverQueryDto;
|
|
262
|
+
__decorate([
|
|
263
|
+
(0, class_validator_1.IsString)(),
|
|
264
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
265
|
+
__metadata("design:type", String)
|
|
266
|
+
], AgencyDiscoverQueryDto.prototype, "q", void 0);
|
|
187
267
|
class AgencyAccessRequestDto {
|
|
188
268
|
constructor(data) {
|
|
189
|
-
this.userId = data
|
|
190
|
-
this.scopes = data
|
|
269
|
+
this.userId = data?.userId ?? '';
|
|
270
|
+
this.scopes = data?.scopes ?? [];
|
|
191
271
|
}
|
|
192
272
|
}
|
|
193
273
|
exports.AgencyAccessRequestDto = AgencyAccessRequestDto;
|
|
274
|
+
__decorate([
|
|
275
|
+
(0, class_validator_1.IsUUID)(),
|
|
276
|
+
__metadata("design:type", String)
|
|
277
|
+
], AgencyAccessRequestDto.prototype, "userId", void 0);
|
|
278
|
+
__decorate([
|
|
279
|
+
(0, class_validator_1.IsArray)(),
|
|
280
|
+
(0, class_validator_1.ArrayNotEmpty)(),
|
|
281
|
+
(0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
|
|
282
|
+
__metadata("design:type", Array)
|
|
283
|
+
], AgencyAccessRequestDto.prototype, "scopes", void 0);
|
|
194
284
|
class AdminSetOrganizationStatusDto {
|
|
195
285
|
constructor(data) {
|
|
196
|
-
this.status = data
|
|
197
|
-
this.reason = data
|
|
286
|
+
this.status = data?.status ?? 'approved';
|
|
287
|
+
this.reason = data?.reason;
|
|
198
288
|
}
|
|
199
289
|
}
|
|
200
290
|
exports.AdminSetOrganizationStatusDto = AdminSetOrganizationStatusDto;
|
|
291
|
+
__decorate([
|
|
292
|
+
(0, class_validator_1.IsIn)(['approved', 'rejected']),
|
|
293
|
+
__metadata("design:type", Object)
|
|
294
|
+
], AdminSetOrganizationStatusDto.prototype, "status", void 0);
|
|
295
|
+
__decorate([
|
|
296
|
+
(0, class_validator_1.IsOptional)(),
|
|
297
|
+
(0, class_validator_1.IsString)(),
|
|
298
|
+
__metadata("design:type", String)
|
|
299
|
+
], AdminSetOrganizationStatusDto.prototype, "reason", void 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ad2app-lib",
|
|
3
|
-
"version": "1.44.
|
|
3
|
+
"version": "1.44.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"prepare": "npm run build"
|
|
52
52
|
},
|
|
53
53
|
"keywords": [],
|
|
54
|
-
"author": "Maciej
|
|
54
|
+
"author": "Maciej G\u00f3rski@ad2.app",
|
|
55
55
|
"license": "ISC",
|
|
56
56
|
"description": "Package to share types and utils across the ad2app projects",
|
|
57
57
|
"dependencies": {
|
package/src/types/agency.test.ts
CHANGED
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
isAgencyGrantScope,
|
|
11
11
|
CreateAgencyOrganizationDto,
|
|
12
12
|
AgencyAccessRequestDto,
|
|
13
|
+
InviteAgencyMemberDto,
|
|
14
|
+
RedeemAgencyInviteDto,
|
|
15
|
+
AcceptAgencyMemberInviteDto,
|
|
16
|
+
AcceptGrantDto,
|
|
17
|
+
UpdateGrantScopesDto,
|
|
18
|
+
SponsorCreatorDto,
|
|
19
|
+
AgencyDiscoverQueryDto,
|
|
20
|
+
AdminSetOrganizationStatusDto,
|
|
13
21
|
} from "./agency";
|
|
14
22
|
|
|
15
23
|
test("the grant scope set is exactly the three reads the spec allows", () => {
|
|
@@ -78,3 +86,53 @@ test("DTOs construct from a plain object (the house `new I_DTO()` shape)", () =>
|
|
|
78
86
|
const request = new AgencyAccessRequestDto({ userId: "u-1", scopes: ["analytics"] });
|
|
79
87
|
assert.deepEqual(request.scopes, ["analytics"]);
|
|
80
88
|
});
|
|
89
|
+
|
|
90
|
+
test("every agency DTO survives being constructed with NOTHING", () => {
|
|
91
|
+
// What a validation pipe actually does. `plainToInstance` calls the
|
|
92
|
+
// constructor with no arguments before copying the body onto it, so a
|
|
93
|
+
// constructor that read `data.name` threw a TypeError on every request
|
|
94
|
+
// instead of validating anything (found 2026-09-22).
|
|
95
|
+
const DTOS = [
|
|
96
|
+
CreateAgencyOrganizationDto,
|
|
97
|
+
InviteAgencyMemberDto,
|
|
98
|
+
RedeemAgencyInviteDto,
|
|
99
|
+
AcceptAgencyMemberInviteDto,
|
|
100
|
+
AcceptGrantDto,
|
|
101
|
+
UpdateGrantScopesDto,
|
|
102
|
+
SponsorCreatorDto,
|
|
103
|
+
AgencyDiscoverQueryDto,
|
|
104
|
+
AgencyAccessRequestDto,
|
|
105
|
+
AdminSetOrganizationStatusDto,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const Dto of DTOS) {
|
|
109
|
+
assert.doesNotThrow(() => new (Dto as new () => unknown)(), `${Dto.name} threw on an empty construct`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("every agency DTO carries validation metadata, not just a shape", () => {
|
|
114
|
+
// An undecorated class typed onto a handler's @Body() is passed through
|
|
115
|
+
// unchecked: the pipe validates DECORATORS, and there were none until
|
|
116
|
+
// 2026-09-22. Read from class-validator's own registry rather than by
|
|
117
|
+
// booting a pipe.
|
|
118
|
+
const { getMetadataStorage } = require("class-validator");
|
|
119
|
+
const storage = getMetadataStorage();
|
|
120
|
+
|
|
121
|
+
const DECORATED = [
|
|
122
|
+
CreateAgencyOrganizationDto,
|
|
123
|
+
InviteAgencyMemberDto,
|
|
124
|
+
RedeemAgencyInviteDto,
|
|
125
|
+
AcceptAgencyMemberInviteDto,
|
|
126
|
+
AcceptGrantDto,
|
|
127
|
+
UpdateGrantScopesDto,
|
|
128
|
+
SponsorCreatorDto,
|
|
129
|
+
AgencyDiscoverQueryDto,
|
|
130
|
+
AgencyAccessRequestDto,
|
|
131
|
+
AdminSetOrganizationStatusDto,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const Dto of DECORATED) {
|
|
135
|
+
const rules = storage.getTargetValidationMetadatas(Dto, Dto.name, false, false);
|
|
136
|
+
assert.ok(rules.length > 0, `${Dto.name} has no validation rules`);
|
|
137
|
+
}
|
|
138
|
+
});
|
package/src/types/agency.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayNotEmpty,
|
|
3
|
+
IsArray,
|
|
4
|
+
IsEmail,
|
|
5
|
+
IsIn,
|
|
6
|
+
IsNotEmpty,
|
|
7
|
+
IsOptional,
|
|
8
|
+
IsString,
|
|
9
|
+
IsUrl,
|
|
10
|
+
IsUUID,
|
|
11
|
+
Length,
|
|
12
|
+
} from 'class-validator';
|
|
13
|
+
|
|
1
14
|
/**
|
|
2
15
|
* Agency domain — organizations, memberships and creator access grants (spec 175).
|
|
3
16
|
*
|
|
@@ -278,43 +291,77 @@ export interface I_SponsoredBy {
|
|
|
278
291
|
|
|
279
292
|
// ── DTOs ─────────────────────────────────────────────────────────────────────
|
|
280
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Every DTO below carries class-validator decorators and a constructor that
|
|
296
|
+
* survives being called with nothing.
|
|
297
|
+
*
|
|
298
|
+
* Both are load-bearing and were missing until 2026-09-22. The backend mounts
|
|
299
|
+
* a global `ValidationPipe`, which validates the decorated metadata of the
|
|
300
|
+
* class a handler's `@Body()` is typed with — an undecorated class, or a body
|
|
301
|
+
* typed as a plain interface, is simply passed through unchecked. And the pipe
|
|
302
|
+
* instantiates the class with NO arguments, so a constructor that reads
|
|
303
|
+
* `data.name` would throw a TypeError on every request rather than validate
|
|
304
|
+
* anything. The rest of this library's DTOs take `data?: Partial<T>` for that
|
|
305
|
+
* reason; these now do too.
|
|
306
|
+
*/
|
|
307
|
+
|
|
281
308
|
export class CreateAgencyOrganizationDto {
|
|
309
|
+
@IsString()
|
|
310
|
+
@IsNotEmpty()
|
|
282
311
|
name: string;
|
|
312
|
+
|
|
313
|
+
/** Rendered as a link on the admin screen, so the protocol is checked here. */
|
|
314
|
+
@IsOptional()
|
|
315
|
+
@IsUrl({ protocols: ['http', 'https'], require_protocol: true })
|
|
283
316
|
website?: string;
|
|
317
|
+
|
|
318
|
+
@IsString()
|
|
319
|
+
@IsNotEmpty()
|
|
284
320
|
registryId: string;
|
|
321
|
+
|
|
322
|
+
/** ISO-3166 alpha-2: which register a reviewer should look in. */
|
|
323
|
+
@IsString()
|
|
324
|
+
@Length(2, 2)
|
|
285
325
|
registryCountry: string;
|
|
326
|
+
|
|
327
|
+
@IsEmail()
|
|
286
328
|
contactEmail: string;
|
|
287
329
|
|
|
288
|
-
constructor(data
|
|
289
|
-
this.name = data
|
|
290
|
-
this.website = data
|
|
291
|
-
this.registryId = data
|
|
292
|
-
this.registryCountry = data
|
|
293
|
-
this.contactEmail = data
|
|
330
|
+
constructor(data?: Partial<CreateAgencyOrganizationDto>) {
|
|
331
|
+
this.name = data?.name ?? '';
|
|
332
|
+
this.website = data?.website;
|
|
333
|
+
this.registryId = data?.registryId ?? '';
|
|
334
|
+
this.registryCountry = data?.registryCountry ?? '';
|
|
335
|
+
this.contactEmail = data?.contactEmail ?? '';
|
|
294
336
|
}
|
|
295
337
|
}
|
|
296
338
|
|
|
297
339
|
export class InviteAgencyMemberDto {
|
|
340
|
+
@IsEmail()
|
|
298
341
|
email: string;
|
|
299
342
|
|
|
300
|
-
constructor(data
|
|
301
|
-
this.email = data
|
|
343
|
+
constructor(data?: Partial<InviteAgencyMemberDto>) {
|
|
344
|
+
this.email = data?.email ?? '';
|
|
302
345
|
}
|
|
303
346
|
}
|
|
304
347
|
|
|
305
348
|
export class RedeemAgencyInviteDto {
|
|
349
|
+
@IsString()
|
|
350
|
+
@IsNotEmpty()
|
|
306
351
|
token: string;
|
|
307
352
|
|
|
308
|
-
constructor(data
|
|
309
|
-
this.token = data
|
|
353
|
+
constructor(data?: Partial<RedeemAgencyInviteDto>) {
|
|
354
|
+
this.token = data?.token ?? '';
|
|
310
355
|
}
|
|
311
356
|
}
|
|
312
357
|
|
|
313
358
|
export class AcceptAgencyMemberInviteDto {
|
|
359
|
+
@IsString()
|
|
360
|
+
@IsNotEmpty()
|
|
314
361
|
token: string;
|
|
315
362
|
|
|
316
|
-
constructor(data
|
|
317
|
-
this.token = data
|
|
363
|
+
constructor(data?: Partial<AcceptAgencyMemberInviteDto>) {
|
|
364
|
+
this.token = data?.token ?? '';
|
|
318
365
|
}
|
|
319
366
|
}
|
|
320
367
|
|
|
@@ -323,60 +370,89 @@ export class AcceptAgencyMemberInviteDto {
|
|
|
323
370
|
* is the creator's answer and defaults to the requested set when absent.
|
|
324
371
|
*/
|
|
325
372
|
export class AcceptGrantDto {
|
|
373
|
+
@IsOptional()
|
|
374
|
+
@IsString()
|
|
375
|
+
@IsNotEmpty()
|
|
326
376
|
token?: string;
|
|
377
|
+
|
|
378
|
+
@IsOptional()
|
|
379
|
+
@IsUUID()
|
|
327
380
|
grantId?: string;
|
|
381
|
+
|
|
382
|
+
/** A subset of the three reads. Never empty: sharing nothing is declining. */
|
|
383
|
+
@IsOptional()
|
|
384
|
+
@IsArray()
|
|
385
|
+
@ArrayNotEmpty()
|
|
386
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
328
387
|
scopes?: I_AgencyGrantScope[];
|
|
329
388
|
|
|
330
|
-
constructor(data
|
|
331
|
-
this.token = data
|
|
332
|
-
this.grantId = data
|
|
333
|
-
this.scopes = data
|
|
389
|
+
constructor(data?: Partial<AcceptGrantDto>) {
|
|
390
|
+
this.token = data?.token;
|
|
391
|
+
this.grantId = data?.grantId;
|
|
392
|
+
this.scopes = data?.scopes;
|
|
334
393
|
}
|
|
335
394
|
}
|
|
336
395
|
|
|
337
396
|
/** The creator narrowing or widening a live grant — the only other write path. */
|
|
338
397
|
export class UpdateGrantScopesDto {
|
|
398
|
+
@IsArray()
|
|
399
|
+
@ArrayNotEmpty()
|
|
400
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
339
401
|
scopes: I_AgencyGrantScope[];
|
|
340
402
|
|
|
341
|
-
constructor(data
|
|
342
|
-
this.scopes = data
|
|
403
|
+
constructor(data?: Partial<UpdateGrantScopesDto>) {
|
|
404
|
+
this.scopes = data?.scopes ?? [];
|
|
343
405
|
}
|
|
344
406
|
}
|
|
345
407
|
|
|
346
408
|
export class SponsorCreatorDto {
|
|
409
|
+
@IsIn(['starter', 'pro'])
|
|
347
410
|
tier: SponsoredTier;
|
|
348
411
|
|
|
349
|
-
constructor(data
|
|
350
|
-
this.tier = data
|
|
412
|
+
constructor(data?: Partial<SponsorCreatorDto>) {
|
|
413
|
+
this.tier = data?.tier ?? 'starter';
|
|
351
414
|
}
|
|
352
415
|
}
|
|
353
416
|
|
|
354
417
|
export class AgencyDiscoverQueryDto {
|
|
355
418
|
/** An EXACT platform username or e-mail. Never recorded in analytics (FR-015). */
|
|
419
|
+
@IsString()
|
|
420
|
+
@IsNotEmpty()
|
|
356
421
|
q: string;
|
|
357
422
|
|
|
358
|
-
constructor(data
|
|
359
|
-
this.q = data
|
|
423
|
+
constructor(data?: Partial<AgencyDiscoverQueryDto>) {
|
|
424
|
+
this.q = data?.q ?? '';
|
|
360
425
|
}
|
|
361
426
|
}
|
|
362
427
|
|
|
363
428
|
export class AgencyAccessRequestDto {
|
|
429
|
+
@IsUUID()
|
|
364
430
|
userId: string;
|
|
431
|
+
|
|
432
|
+
@IsArray()
|
|
433
|
+
@ArrayNotEmpty()
|
|
434
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
365
435
|
scopes: I_AgencyGrantScope[];
|
|
366
436
|
|
|
367
|
-
constructor(data
|
|
368
|
-
this.userId = data
|
|
369
|
-
this.scopes = data
|
|
437
|
+
constructor(data?: Partial<AgencyAccessRequestDto>) {
|
|
438
|
+
this.userId = data?.userId ?? '';
|
|
439
|
+
this.scopes = data?.scopes ?? [];
|
|
370
440
|
}
|
|
371
441
|
}
|
|
372
442
|
|
|
373
443
|
export class AdminSetOrganizationStatusDto {
|
|
444
|
+
@IsIn(['approved', 'rejected'])
|
|
374
445
|
status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
|
|
375
|
-
|
|
446
|
+
|
|
447
|
+
/** Required on reject — a rejected applicant is shown this (FR-002). The
|
|
448
|
+
* "required when rejecting" half is a rule about the PAIR, so it stays in
|
|
449
|
+
* the service where both fields are in hand. */
|
|
450
|
+
@IsOptional()
|
|
451
|
+
@IsString()
|
|
376
452
|
reason?: string;
|
|
377
453
|
|
|
378
|
-
constructor(data
|
|
379
|
-
this.status = data
|
|
380
|
-
this.reason = data
|
|
454
|
+
constructor(data?: Partial<AdminSetOrganizationStatusDto>) {
|
|
455
|
+
this.status = data?.status ?? 'approved';
|
|
456
|
+
this.reason = data?.reason;
|
|
381
457
|
}
|
|
382
458
|
}
|