bazis 0.96.6 → 0.97.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/cli/templates/module.ts +13 -12
- package/cli/templates/passport.ts +1 -1
- package/core/di/README.md +1 -1
- package/core/di/index.ts +1 -0
- package/core/di/token.ts +9 -1
- package/core/http/Authorization/Authorize.ts +3 -2
- package/core/http/Authorization/metadata.ts +17 -11
- package/core/http/SPEC.md +7 -2
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/package.json +1 -1
package/cli/templates/module.ts
CHANGED
|
@@ -194,23 +194,24 @@ export class ${n.module}DbContext extends DbContext {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function serviceInterfaceFile(n: ModuleNaming): string {
|
|
197
|
-
return `import {
|
|
198
|
-
import type { PageResult } from "bazis/core/orm";
|
|
197
|
+
return `import type { PageResult } from "bazis/core/orm";
|
|
199
198
|
import type { ListQuery } from "bazis/library/jsonapi";
|
|
200
199
|
import type { Create${n.entity}Request, Update${n.entity}Request } from "../http/contracts/${n.module}.requests";
|
|
201
200
|
import type { ${n.entity}Response, ${n.module}Summary } from "../http/contracts/${n.module}.responses";
|
|
202
201
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Service contract. An abstract class, not an interface: it exists at runtime,
|
|
204
|
+
* so it is the DI token itself: \`scoped(I${n.module}Service, ${n.module}Service)\`.
|
|
205
|
+
*/
|
|
206
|
+
export abstract class I${n.module}Service {
|
|
207
|
+
abstract getAll(query: ListQuery): Promise<PageResult<${n.entity}Response>>;
|
|
208
|
+
abstract summary(): Promise<${n.module}Summary>;
|
|
209
|
+
abstract count(): Promise<number>;
|
|
210
|
+
abstract getById(id: string): Promise<${n.entity}Response | null>;
|
|
211
|
+
abstract create(body: Create${n.entity}Request): Promise<${n.entity}Response>;
|
|
212
|
+
abstract update(id: string, body: Update${n.entity}Request): Promise<${n.entity}Response | null>;
|
|
213
|
+
abstract delete(id: string): Promise<boolean>;
|
|
211
214
|
}
|
|
212
|
-
|
|
213
|
-
export const I${n.module}Service = createToken<I${n.module}Service>("I${n.module}Service");
|
|
214
215
|
`;
|
|
215
216
|
}
|
|
216
217
|
|
|
@@ -51,7 +51,7 @@ adapters that access it.
|
|
|
51
51
|
| ${n.moduleClass} | [${n.module}.module.ts](${n.module}.module.ts) | host imports | ORM, DI, HTTP${profile === "full" ? ", background, AI" : ""} |
|
|
52
52
|
| ${e} | [model/${m}.model.ts](model/${m}.model.ts) | Fields below | Table ${n.route} |
|
|
53
53
|
| ${m}DbContext | [model/${m}.dbContext.ts](model/${m}.dbContext.ts) | Shared host ORM provider | DbSet ${n.collection} |
|
|
54
|
-
| I${m}Service / ${m}Service | [services/${m}.service.ts](services/${m}.service.ts), [
|
|
54
|
+
| I${m}Service / ${m}Service | [services/${m}.service.ts](services/${m}.service.ts), [contract](services/I${m}.service.ts) | ${m}DbContext${profile === "full" ? ", ICache" : ""} | CRUD, count, summary |
|
|
55
55
|
| ${m}Controller | [http/${m}.controller.ts](http/${m}.controller.ts) | I${m}Service, HTTP request | HTTP operations below |
|
|
56
56
|
| Create${e}Request / Update${e}Request | [requests](http/contracts/${m}.requests.ts) | JSON body | RequestModel + Validator |
|
|
57
57
|
| ${e}Response / ${m}Summary / to${e}Response | [responses](http/contracts/${m}.responses.ts) | ORM entity | Public data projection |
|
package/core/di/README.md
CHANGED
|
@@ -14,7 +14,7 @@ contracts are described in [MODULE.md](MODULE.md).
|
|
|
14
14
|
- `ServiceCollection.ts`: service registration.
|
|
15
15
|
- `ServiceScope.ts`: scoped resolve and scoped dispose.
|
|
16
16
|
- `container.ts`: a compatible alias wrapper over `ServiceProvider`.
|
|
17
|
-
- `token.ts`: DI tokens
|
|
17
|
+
- `token.ts`: DI tokens: classes, abstract classes as service contracts, `createToken`, open generic family.
|
|
18
18
|
|
|
19
19
|
## Internals (do not use directly)
|
|
20
20
|
|
package/core/di/index.ts
CHANGED
package/core/di/token.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
// constructor deps is still validated by the container.
|
|
4
4
|
export type Class<T> = new (...args: any[]) => T;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A class usable only as a key: an abstract class declared as a contract
|
|
8
|
+
* (`export abstract class IClock { abstract now(): Date }`) and registered with
|
|
9
|
+
* `scoped(IClock, SystemClock)`. Unlike an interface it exists at runtime, so it
|
|
10
|
+
* needs no `createToken`. The container never constructs it.
|
|
11
|
+
*/
|
|
12
|
+
export type AbstractClass<T> = abstract new (...args: any[]) => T;
|
|
13
|
+
|
|
6
14
|
export interface InjectionToken<T> {
|
|
7
15
|
readonly id: symbol;
|
|
8
16
|
readonly description: string;
|
|
@@ -18,7 +26,7 @@ export function createToken<T>(description: string): InjectionToken<T> {
|
|
|
18
26
|
};
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
export type Token<T> = Class<T> | InjectionToken<T>;
|
|
29
|
+
export type Token<T> = Class<T> | AbstractClass<T> | InjectionToken<T>;
|
|
22
30
|
|
|
23
31
|
export interface OpenGenericTokenFamily<TArg, TResult> {
|
|
24
32
|
readonly id: symbol;
|
|
@@ -17,7 +17,8 @@ type ClassOrMethodDecorator = (
|
|
|
17
17
|
* Protects a controller or a single action: the route gets the
|
|
18
18
|
* {@link AuthorizeCheck} function. The HTTP kernel does not know *what* it
|
|
19
19
|
* checks: JWT, login/password, Telegram sign-in and so on. Several checks
|
|
20
|
-
* can be passed; all of them must pass.
|
|
20
|
+
* can be passed; all of them must pass. On a method of a protected controller
|
|
21
|
+
* the checks are added to the controller's ones: the class checks run first.
|
|
21
22
|
*
|
|
22
23
|
* ```ts
|
|
23
24
|
* @Authorize(requireTokenKind(TokenKind.Admin)) // access token kind
|
|
@@ -40,7 +41,7 @@ export function Authorize(check: AuthorizeCheck, ...rest: AuthorizeCheck[]): Cla
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/** Public route:
|
|
44
|
+
/** Public route: on a method, removes every check, including the controller's `@Authorize`. */
|
|
44
45
|
export function AllowAnonymous(): ClassOrMethodDecorator {
|
|
45
46
|
return (_value, context) => {
|
|
46
47
|
if (context.kind === "class") {
|
|
@@ -122,15 +122,22 @@ export function setActionAllowAnonymous(metadata: object, methodName: string | s
|
|
|
122
122
|
delete action.authorize;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
/** Effective route requirement
|
|
125
|
+
/** Effective route requirement: the class and method checks combined. */
|
|
126
126
|
export interface ResolvedAuthorizeMeta {
|
|
127
127
|
readonly allowAnonymous: boolean;
|
|
128
128
|
readonly authorize?: AuthorizeOptions;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Resolves the authorization requirement of a specific action
|
|
133
|
-
*
|
|
132
|
+
* Resolves the authorization requirement of a specific action:
|
|
133
|
+
*
|
|
134
|
+
* - `@AllowAnonymous` on the method opens it, whatever the class declares;
|
|
135
|
+
* - `@Authorize` on the class and on the method combine: the class checks run
|
|
136
|
+
* first, then the method checks (a check repeated on both runs once);
|
|
137
|
+
* - `@AllowAnonymous` on the class opens the methods without their own `@Authorize`.
|
|
138
|
+
*
|
|
139
|
+
* Inheritance is different: a subclass's own class-level declaration replaces
|
|
140
|
+
* the base class's, and an overriding method's declaration replaces the base method's.
|
|
134
141
|
*/
|
|
135
142
|
export function resolveAuthorizeMeta(ctor: object, methodName: string | symbol): ResolvedAuthorizeMeta {
|
|
136
143
|
const metadata = (ctor as { [key: symbol]: unknown })[Symbol.metadata as unknown as symbol] as
|
|
@@ -145,14 +152,13 @@ export function resolveAuthorizeMeta(ctor: object, methodName: string | symbol):
|
|
|
145
152
|
if (action?.allowAnonymous) {
|
|
146
153
|
return { allowAnonymous: true };
|
|
147
154
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (meta.allowAnonymous) {
|
|
155
|
+
const classChecks = meta.authorize?.checks ?? [];
|
|
156
|
+
const actionChecks = action?.authorize?.checks ?? [];
|
|
157
|
+
if (actionChecks.length === 0 && meta.allowAnonymous) {
|
|
152
158
|
return { allowAnonymous: true };
|
|
153
159
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
const checks = [...new Set([...classChecks, ...actionChecks])];
|
|
161
|
+
return checks.length === 0
|
|
162
|
+
? { allowAnonymous: false }
|
|
163
|
+
: { allowAnonymous: false, authorize: { checks } };
|
|
158
164
|
}
|
package/core/http/SPEC.md
CHANGED
|
@@ -804,8 +804,13 @@ Do not create a **new** middleware function per request, only at load/startup.
|
|
|
804
804
|
`@Authorize(check, ...checks)` on a controller or a method compiles into the built-in
|
|
805
805
|
authorization link [6]. A check is `(ctx) => boolean | Promise<boolean>`: `true` lets the
|
|
806
806
|
request through, `false` gives `403`, and a thrown `HttpError` defines the response
|
|
807
|
-
itself (for example `UnauthorizedError` → `401`).
|
|
808
|
-
|
|
807
|
+
itself (for example `UnauthorizedError` → `401`). Checks of the controller and of the
|
|
808
|
+
method combine: the controller's run first, then the method's; a check repeated on both
|
|
809
|
+
runs once. A method-level `@AllowAnonymous` removes every check, including the
|
|
810
|
+
controller's. On an `@AllowAnonymous` controller only methods with their own
|
|
811
|
+
`@Authorize` are protected. Inheritance replaces instead: a subclass's own class-level
|
|
812
|
+
declaration replaces the base class's (since 0.97.0; before it a method-level
|
|
813
|
+
`@Authorize` replaced the controller's checks).
|
|
809
814
|
|
|
810
815
|
```ts
|
|
811
816
|
const isSignedIn: AuthorizeCheck = (ctx) => {
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.97.0" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
package/package.json
CHANGED