@supacloud/app 0.1.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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @supacloud/app
2
+
3
+ Angular-style application metadata for SupaCloud applications: modules, DI
4
+ tokens, providers, scopes, controllers and commands.
5
+
6
+ This package is **metadata only**. Decorators attach metadata to classes; the
7
+ SupaCloud compiler (`@supacloud/compiler`) reads that metadata from source,
8
+ validates the dependency graph and generates plain static factories — there is
9
+ no runtime reflection and no `reflect-metadata` dependency.
10
+
11
+ ```ts
12
+ import {
13
+ Command,
14
+ Controller,
15
+ Inject,
16
+ Injectable,
17
+ InjectionToken,
18
+ Module,
19
+ Post,
20
+ } from "@supacloud/app";
21
+
22
+ export const CASE_REPOSITORY = new InjectionToken<CaseRepository>("case.repository");
23
+
24
+ @Injectable()
25
+ export class CaseService {
26
+ constructor(
27
+ @Inject(CASE_REPOSITORY) private readonly repository: CaseRepository,
28
+ ) {}
29
+ }
30
+
31
+ @Command({ name: "case.accept", permission: "case.accept", transaction: "required" })
32
+ export class AcceptCaseCommand {
33
+ constructor(private readonly cases: CaseService) {}
34
+ }
35
+
36
+ @Controller("/cases")
37
+ export class CaseController {
38
+ constructor(private readonly acceptCase: AcceptCaseCommand) {}
39
+
40
+ @Post("/:caseId/accept", { body: CaseAcceptInput })
41
+ accept() {
42
+ return this.acceptCase.execute();
43
+ }
44
+ }
45
+
46
+ @Module({
47
+ name: "case",
48
+ providers: [
49
+ CaseService,
50
+ { provide: CASE_REPOSITORY, useClass: DrizzleCaseRepository },
51
+ AcceptCaseCommand,
52
+ ],
53
+ controllers: [CaseController],
54
+ exports: [CaseService],
55
+ })
56
+ export class CaseModule {}
57
+ ```
58
+
59
+ ## Scopes
60
+
61
+ | Scope | Lifetime | May depend on |
62
+ |---|---|---|
63
+ | `application` (default) | whole function instance | `application` only |
64
+ | `request` | one HTTP request | `application`, `request` |
65
+ | `job` | one background task | `application`, `job` |
66
+
67
+ The compiler rejects scope violations (e.g. an `application` provider
68
+ depending on a `request` provider) at build time.
69
+
70
+ ## Non-decorator usage
71
+
72
+ `defineModule(options)` produces the same metadata as `@Module(options)` and
73
+ can be used where decorators are not enabled.
@@ -0,0 +1,14 @@
1
+ import { InjectionToken } from "./token";
2
+ /**
3
+ * Built-in token resolved from the request-context argument of a compiled
4
+ * `createRequestScope(services, ctx)` factory. Request-scoped providers that
5
+ * declare this token as a dependency receive the raw request context.
6
+ */
7
+ export declare const REQUEST_CONTEXT: InjectionToken<unknown>;
8
+ /**
9
+ * Built-in token resolved from the job-context argument of a compiled
10
+ * `createJobScope(services, ctx)` factory. Job-scoped providers that declare
11
+ * this token as a dependency receive the raw job context (task id, lease,
12
+ * cancellation signal, ...).
13
+ */
14
+ export declare const JOB_CONTEXT: InjectionToken<unknown>;
@@ -0,0 +1,94 @@
1
+ import type { Provider, Token, Type } from "./provider";
2
+ import type { Scope } from "./scope";
3
+ /**
4
+ * Decorator metadata keys. Metadata is attached as static properties on the
5
+ * decorated class so the SupaCloud compiler can read it from the AST *and*
6
+ * tooling can read it at runtime without reflect-metadata.
7
+ */
8
+ export declare const INJECTABLE_METADATA = "supacloud:injectable";
9
+ export declare const MODULE_METADATA = "supacloud:module";
10
+ export declare const COMMAND_METADATA = "supacloud:command";
11
+ export declare const QUERY_METADATA = "supacloud:query";
12
+ export declare const CONTROLLER_METADATA = "supacloud:controller";
13
+ export declare const ROUTES_METADATA = "supacloud:routes";
14
+ export declare const INJECT_PARAMS_METADATA = "supacloud:inject-params";
15
+ export interface InjectableOptions {
16
+ scope?: Scope;
17
+ /** Explicit constructor dependency tokens, in parameter order. */
18
+ deps?: Token[];
19
+ }
20
+ export interface InjectableMeta {
21
+ scope: Scope;
22
+ deps: Token[];
23
+ }
24
+ export interface ModuleOptions {
25
+ name: string;
26
+ imports?: Array<Type<unknown>>;
27
+ providers?: Provider[];
28
+ controllers?: Array<Type<unknown>>;
29
+ commands?: Array<Type<unknown>>;
30
+ queries?: Array<Type<unknown>>;
31
+ exports?: Token[];
32
+ }
33
+ export interface ModuleMeta extends Required<Omit<ModuleOptions, "exports">> {
34
+ exports: Token[];
35
+ }
36
+ export interface CommandOptions {
37
+ name: string;
38
+ /** Permission identifier required to execute (e.g. "case.create"). */
39
+ permission?: string;
40
+ /** Transaction requirement for the underlying write, e.g. "required". */
41
+ transaction?: string;
42
+ /** Audit event name recorded on success, e.g. "case.created". */
43
+ audit?: string;
44
+ /** Idempotency strategy, e.g. "required". */
45
+ idempotency?: string;
46
+ }
47
+ export type CommandMeta = CommandOptions;
48
+ export interface QueryOptions {
49
+ name: string;
50
+ }
51
+ export type QueryMeta = QueryOptions;
52
+ export interface ControllerMeta {
53
+ path: string;
54
+ }
55
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
56
+ export interface RouteOptions {
57
+ /** TypeBox schema (or compatible) for the request body. */
58
+ body?: unknown;
59
+ /** TypeBox schema for path params. */
60
+ params?: unknown;
61
+ /** TypeBox schema for query string. */
62
+ query?: unknown;
63
+ /** TypeBox schema for the response. */
64
+ response?: unknown;
65
+ }
66
+ export interface RouteDefinition extends RouteOptions {
67
+ method: HttpMethod;
68
+ path: string;
69
+ /** Controller method name. */
70
+ handler: string;
71
+ }
72
+ export declare function Injectable(options?: InjectableOptions): ClassDecorator;
73
+ export declare function getInjectableMeta(target: object): InjectableMeta | undefined;
74
+ /**
75
+ * Parameter decorator marking a constructor parameter's injection token.
76
+ * Only meaningful together with the SupaCloud compiler; no runtime reflection
77
+ * is performed.
78
+ */
79
+ export declare function Inject(token: Token): ParameterDecorator;
80
+ export declare function getInjectParams(target: object): Record<number, Token>;
81
+ export declare function Module(options: ModuleOptions): ClassDecorator;
82
+ export declare function getModuleMeta(target: object): ModuleMeta | undefined;
83
+ export declare function Command(options: CommandOptions): ClassDecorator;
84
+ export declare function getCommandMeta(target: object): CommandMeta | undefined;
85
+ export declare function Query(options: QueryOptions): ClassDecorator;
86
+ export declare function getQueryMeta(target: object): QueryMeta | undefined;
87
+ export declare function Controller(path: string): ClassDecorator;
88
+ export declare function getControllerMeta(target: object): ControllerMeta | undefined;
89
+ export declare const Get: (path: string, options?: RouteOptions) => MethodDecorator;
90
+ export declare const Post: (path: string, options?: RouteOptions) => MethodDecorator;
91
+ export declare const Put: (path: string, options?: RouteOptions) => MethodDecorator;
92
+ export declare const Patch: (path: string, options?: RouteOptions) => MethodDecorator;
93
+ export declare const Delete: (path: string, options?: RouteOptions) => MethodDecorator;
94
+ export declare function getRoutes(target: object): RouteDefinition[];
@@ -0,0 +1,10 @@
1
+ export { SCOPES, DEFAULT_SCOPE, SCOPE_LIFETIME_RANK, isScopeViolation } from "./scope";
2
+ export type { Scope } from "./scope";
3
+ export { InjectionToken } from "./token";
4
+ export type { InjectionTokenOptions } from "./token";
5
+ export { isClassProvider, isExistingProvider, isFactoryProvider, isValueProvider, } from "./provider";
6
+ export type { ClassProvider, ExistingProvider, FactoryProvider, Provider, Token, Type, ValueProvider, } from "./provider";
7
+ export { Command, Controller, Delete, Get, Inject, Injectable, Module, Patch, Post, Put, Query, getCommandMeta, getControllerMeta, getInjectParams, getInjectableMeta, getModuleMeta, getQueryMeta, getRoutes, COMMAND_METADATA, CONTROLLER_METADATA, INJECTABLE_METADATA, INJECT_PARAMS_METADATA, MODULE_METADATA, QUERY_METADATA, ROUTES_METADATA, } from "./decorators";
8
+ export type { CommandMeta, CommandOptions, ControllerMeta, HttpMethod, InjectableMeta, InjectableOptions, ModuleMeta, ModuleOptions, QueryMeta, QueryOptions, RouteDefinition, RouteOptions, } from "./decorators";
9
+ export { defineModule } from "./module";
10
+ export { JOB_CONTEXT, REQUEST_CONTEXT } from "./context";
package/dist/index.js ADDED
@@ -0,0 +1,205 @@
1
+ // src/scope.ts
2
+ var SCOPES = ["application", "request", "job"];
3
+ var DEFAULT_SCOPE = "application";
4
+ var SCOPE_LIFETIME_RANK = {
5
+ application: 0,
6
+ request: 1,
7
+ job: 1
8
+ };
9
+ function isScopeViolation(from, to) {
10
+ return SCOPE_LIFETIME_RANK[to] > SCOPE_LIFETIME_RANK[from];
11
+ }
12
+ // src/token.ts
13
+ class InjectionToken {
14
+ name;
15
+ factory;
16
+ scope;
17
+ constructor(name, options = {}) {
18
+ this.name = name;
19
+ this.factory = options.factory;
20
+ this.scope = options.scope;
21
+ }
22
+ toString() {
23
+ return `InjectionToken ${this.name}`;
24
+ }
25
+ }
26
+ // src/provider.ts
27
+ function isClassProvider(provider) {
28
+ return typeof provider === "object" && provider !== null && "useClass" in provider;
29
+ }
30
+ function isValueProvider(provider) {
31
+ return typeof provider === "object" && provider !== null && "useValue" in provider;
32
+ }
33
+ function isFactoryProvider(provider) {
34
+ return typeof provider === "object" && provider !== null && "useFactory" in provider;
35
+ }
36
+ function isExistingProvider(provider) {
37
+ return typeof provider === "object" && provider !== null && "useExisting" in provider;
38
+ }
39
+ // src/decorators.ts
40
+ var INJECTABLE_METADATA = "supacloud:injectable";
41
+ var MODULE_METADATA = "supacloud:module";
42
+ var COMMAND_METADATA = "supacloud:command";
43
+ var QUERY_METADATA = "supacloud:query";
44
+ var CONTROLLER_METADATA = "supacloud:controller";
45
+ var ROUTES_METADATA = "supacloud:routes";
46
+ var INJECT_PARAMS_METADATA = "supacloud:inject-params";
47
+ function defineMetadata(target, key, value) {
48
+ Object.defineProperty(target, key, {
49
+ value,
50
+ configurable: true,
51
+ writable: true
52
+ });
53
+ }
54
+ function readOwnOrInherited(target, key) {
55
+ return target[key];
56
+ }
57
+ function Injectable(options = {}) {
58
+ return (target) => {
59
+ const meta = {
60
+ scope: options.scope ?? DEFAULT_SCOPE,
61
+ deps: options.deps ?? []
62
+ };
63
+ defineMetadata(target, INJECTABLE_METADATA, meta);
64
+ };
65
+ }
66
+ function getInjectableMeta(target) {
67
+ return readOwnOrInherited(target, INJECTABLE_METADATA);
68
+ }
69
+ function Inject(token) {
70
+ return (target, propertyKey, parameterIndex) => {
71
+ if (propertyKey !== undefined) {
72
+ throw new Error("@Inject() is only supported on constructor parameters");
73
+ }
74
+ const cls = target;
75
+ const meta = {
76
+ ...readOwnOrInherited(cls, INJECT_PARAMS_METADATA)
77
+ };
78
+ meta[parameterIndex] = token;
79
+ defineMetadata(cls, INJECT_PARAMS_METADATA, meta);
80
+ };
81
+ }
82
+ function getInjectParams(target) {
83
+ return readOwnOrInherited(target, INJECT_PARAMS_METADATA) ?? {};
84
+ }
85
+ function Module(options) {
86
+ return (target) => {
87
+ const meta = {
88
+ name: options.name,
89
+ imports: options.imports ?? [],
90
+ providers: options.providers ?? [],
91
+ controllers: options.controllers ?? [],
92
+ commands: options.commands ?? [],
93
+ queries: options.queries ?? [],
94
+ exports: options.exports ?? []
95
+ };
96
+ defineMetadata(target, MODULE_METADATA, meta);
97
+ };
98
+ }
99
+ function getModuleMeta(target) {
100
+ return readOwnOrInherited(target, MODULE_METADATA);
101
+ }
102
+ function Command(options) {
103
+ return (target) => {
104
+ defineMetadata(target, COMMAND_METADATA, { ...options });
105
+ };
106
+ }
107
+ function getCommandMeta(target) {
108
+ return readOwnOrInherited(target, COMMAND_METADATA);
109
+ }
110
+ function Query(options) {
111
+ return (target) => {
112
+ defineMetadata(target, QUERY_METADATA, { ...options });
113
+ };
114
+ }
115
+ function getQueryMeta(target) {
116
+ return readOwnOrInherited(target, QUERY_METADATA);
117
+ }
118
+ function Controller(path) {
119
+ return (target) => {
120
+ defineMetadata(target, CONTROLLER_METADATA, { path });
121
+ };
122
+ }
123
+ function getControllerMeta(target) {
124
+ return readOwnOrInherited(target, CONTROLLER_METADATA);
125
+ }
126
+ function createRouteDecorator(method) {
127
+ return (path, options = {}) => (target, propertyKey) => {
128
+ const cls = target.constructor;
129
+ const routes = [
130
+ ...readOwnOrInherited(cls, ROUTES_METADATA) ?? []
131
+ ];
132
+ routes.push({
133
+ method,
134
+ path,
135
+ handler: String(propertyKey),
136
+ ...options
137
+ });
138
+ defineMetadata(cls, ROUTES_METADATA, routes);
139
+ };
140
+ }
141
+ var Get = createRouteDecorator("GET");
142
+ var Post = createRouteDecorator("POST");
143
+ var Put = createRouteDecorator("PUT");
144
+ var Patch = createRouteDecorator("PATCH");
145
+ var Delete = createRouteDecorator("DELETE");
146
+ function getRoutes(target) {
147
+ return readOwnOrInherited(target, ROUTES_METADATA) ?? [];
148
+ }
149
+ // src/module.ts
150
+ function defineModule(options) {
151
+ class DefinedModule {
152
+ }
153
+ Object.defineProperty(DefinedModule, "name", {
154
+ value: options.name,
155
+ configurable: true
156
+ });
157
+ Module(options)(DefinedModule);
158
+ return DefinedModule;
159
+ }
160
+ // src/context.ts
161
+ var REQUEST_CONTEXT = new InjectionToken("supacloud.request-context", {
162
+ scope: "request"
163
+ });
164
+ var JOB_CONTEXT = new InjectionToken("supacloud.job-context", {
165
+ scope: "job"
166
+ });
167
+ export {
168
+ COMMAND_METADATA,
169
+ CONTROLLER_METADATA,
170
+ Command,
171
+ Controller,
172
+ DEFAULT_SCOPE,
173
+ Delete,
174
+ Get,
175
+ INJECTABLE_METADATA,
176
+ INJECT_PARAMS_METADATA,
177
+ Inject,
178
+ Injectable,
179
+ InjectionToken,
180
+ JOB_CONTEXT,
181
+ MODULE_METADATA,
182
+ Module,
183
+ Patch,
184
+ Post,
185
+ Put,
186
+ QUERY_METADATA,
187
+ Query,
188
+ REQUEST_CONTEXT,
189
+ ROUTES_METADATA,
190
+ SCOPES,
191
+ SCOPE_LIFETIME_RANK,
192
+ defineModule,
193
+ getCommandMeta,
194
+ getControllerMeta,
195
+ getInjectParams,
196
+ getInjectableMeta,
197
+ getModuleMeta,
198
+ getQueryMeta,
199
+ getRoutes,
200
+ isClassProvider,
201
+ isExistingProvider,
202
+ isFactoryProvider,
203
+ isScopeViolation,
204
+ isValueProvider
205
+ };
@@ -0,0 +1,8 @@
1
+ import type { ModuleOptions } from "./decorators";
2
+ import type { Type } from "./provider";
3
+ /**
4
+ * Functional equivalent of the `@Module()` decorator for codebases that do
5
+ * not enable `experimentalDecorators`. Returns a class carrying the same
6
+ * module metadata, so it can be used interchangeably in `imports` arrays.
7
+ */
8
+ export declare function defineModule(options: ModuleOptions): Type<unknown>;
@@ -0,0 +1,38 @@
1
+ import type { InjectionToken } from "./token";
2
+ import type { Scope } from "./scope";
3
+ /** A class usable as a DI token / provider implementation. */
4
+ export interface Type<T> {
5
+ new (...args: any[]): T;
6
+ }
7
+ /** Anything that can identify a provider: an InjectionToken or a class. */
8
+ export type Token<T = any> = InjectionToken<T> | Type<T>;
9
+ interface BaseProvider {
10
+ /** Overrides the scope derived from @Injectable / token defaults. */
11
+ scope?: Scope;
12
+ }
13
+ export interface ClassProvider<T = any> extends BaseProvider {
14
+ provide: Token<T>;
15
+ useClass: Type<T>;
16
+ /** Explicit dependency tokens, positional (constructor order). */
17
+ deps?: Token[];
18
+ }
19
+ export interface ValueProvider<T = any> extends BaseProvider {
20
+ provide: Token<T>;
21
+ useValue: T;
22
+ }
23
+ export interface FactoryProvider<T = any> extends BaseProvider {
24
+ provide: Token<T>;
25
+ useFactory: (...deps: any[]) => T;
26
+ deps?: Token[];
27
+ }
28
+ export interface ExistingProvider<T = any> extends BaseProvider {
29
+ provide: Token<T>;
30
+ useExisting: Token<T>;
31
+ }
32
+ /** Class shorthand registers the class as its own token. */
33
+ export type Provider<T = any> = Type<T> | ClassProvider<T> | ValueProvider<T> | FactoryProvider<T> | ExistingProvider<T>;
34
+ export declare function isClassProvider<T>(provider: Provider<T>): provider is ClassProvider<T>;
35
+ export declare function isValueProvider<T>(provider: Provider<T>): provider is ValueProvider<T>;
36
+ export declare function isFactoryProvider<T>(provider: Provider<T>): provider is FactoryProvider<T>;
37
+ export declare function isExistingProvider<T>(provider: Provider<T>): provider is ExistingProvider<T>;
38
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Provider lifetime scopes.
3
+ *
4
+ * `application` providers live for the whole function instance and must never
5
+ * depend on shorter-lived `request` / `job` providers. `request` providers are
6
+ * created per HTTP request, `job` providers per background task execution.
7
+ */
8
+ export declare const SCOPES: readonly ["application", "request", "job"];
9
+ export type Scope = (typeof SCOPES)[number];
10
+ export declare const DEFAULT_SCOPE: Scope;
11
+ /**
12
+ * Lifetime rank: longer-lived scopes have a lower rank. A provider may only
13
+ * depend on providers of the same or a longer-lived (lower rank) scope.
14
+ */
15
+ export declare const SCOPE_LIFETIME_RANK: Record<Scope, number>;
16
+ export declare function isScopeViolation(from: Scope, to: Scope): boolean;
@@ -0,0 +1,18 @@
1
+ import type { Scope } from "./scope";
2
+ export interface InjectionTokenOptions<T> {
3
+ /** Optional default factory used when no explicit provider is registered. */
4
+ factory?: () => T;
5
+ /** Default scope when the token itself is used as a provider. */
6
+ scope?: Scope;
7
+ }
8
+ /**
9
+ * Lightweight injection token, modeled after Angular's tree-shakable tokens.
10
+ * Used as a DI key for interfaces and values that are not classes.
11
+ */
12
+ export declare class InjectionToken<T> {
13
+ readonly name: string;
14
+ readonly factory?: () => T;
15
+ readonly scope?: Scope;
16
+ constructor(name: string, options?: InjectionTokenOptions<T>);
17
+ toString(): string;
18
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@supacloud/app",
3
+ "version": "0.1.0",
4
+ "description": "Angular-style application metadata for SupaCloud: modules, DI tokens, providers, scopes, controllers and commands",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "bun run clean && bun run build:js && bun run build:types",
22
+ "build:js": "bun build src/index.ts --outdir dist --target node",
23
+ "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
24
+ "clean": "rm -rf dist",
25
+ "prepublishOnly": "bun run build",
26
+ "test": "bun test",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit",
28
+ "typecheck:test": "tsc -p tsconfig.test.json --noEmit"
29
+ },
30
+ "keywords": [
31
+ "supacloud",
32
+ "di",
33
+ "dependency-injection",
34
+ "module",
35
+ "elysia"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/vibeunion/supacloud.git",
41
+ "directory": "packages/app"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "^1.4.0",
45
+ "typescript": "^7.0.2"
46
+ }
47
+ }