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.
@@ -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 { createToken } from "bazis/core/di";
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
- export interface I${n.module}Service {
204
- getAll(query: ListQuery): Promise<PageResult<${n.entity}Response>>;
205
- summary(): Promise<${n.module}Summary>;
206
- count(): Promise<number>;
207
- getById(id: string): Promise<${n.entity}Response | null>;
208
- create(body: Create${n.entity}Request): Promise<${n.entity}Response>;
209
- update(id: string, body: Update${n.entity}Request): Promise<${n.entity}Response | null>;
210
- delete(id: string): Promise<boolean>;
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), [token](services/I${m}.service.ts) | ${m}DbContext${profile === "full" ? ", ICache" : ""} | CRUD, count, summary |
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 (`createToken`, open generic family).
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
@@ -75,6 +75,7 @@ export {
75
75
  export {
76
76
  createOpenGenericTokenFamily,
77
77
  createToken,
78
+ type AbstractClass,
78
79
  type Class,
79
80
  type InjectionToken,
80
81
  type OpenGenericTokenFamily,
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: overrides `@Authorize` declared on the controller. */
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 (the method overrides the controller). */
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. Precedence:
133
- * `@AllowAnonymous`/`@Authorize` on the method override those declared on the class.
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
- if (action?.authorize) {
149
- return { allowAnonymous: false, authorize: action.authorize };
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
- if (meta.authorize) {
155
- return { allowAnonymous: false, authorize: meta.authorize };
156
- }
157
- return { allowAnonymous: false };
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`). A method-level `@Authorize` or
808
- `@AllowAnonymous` overrides the controller's.
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.96.6" }, capabilities: { experimentalApi: true },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.96.6",
3
+ "version": "0.97.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",