irgen 0.2.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/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +312 -0
- package/dist/cli.js.map +1 -0
- package/dist/dsl/aggregator.d.ts +8 -0
- package/dist/dsl/aggregator.d.ts.map +1 -0
- package/dist/dsl/aggregator.js +64 -0
- package/dist/dsl/aggregator.js.map +1 -0
- package/dist/dsl/frontend-runtime.d.ts +486 -0
- package/dist/dsl/frontend-runtime.d.ts.map +1 -0
- package/dist/dsl/frontend-runtime.js +232 -0
- package/dist/dsl/frontend-runtime.js.map +1 -0
- package/dist/dsl/runtime.d.ts +33 -0
- package/dist/dsl/runtime.d.ts.map +1 -0
- package/dist/dsl/runtime.js +120 -0
- package/dist/dsl/runtime.js.map +1 -0
- package/dist/emit/backend/adapters.d.ts +11 -0
- package/dist/emit/backend/adapters.d.ts.map +1 -0
- package/dist/emit/backend/adapters.js +374 -0
- package/dist/emit/backend/adapters.js.map +1 -0
- package/dist/emit/backend/backend-tsmorph.d.ts +5 -0
- package/dist/emit/backend/backend-tsmorph.d.ts.map +1 -0
- package/dist/emit/backend/backend-tsmorph.js +858 -0
- package/dist/emit/backend/backend-tsmorph.js.map +1 -0
- package/dist/emit/backend/fake-backend.d.ts +2 -0
- package/dist/emit/backend/fake-backend.d.ts.map +1 -0
- package/dist/emit/backend/fake-backend.js +19 -0
- package/dist/emit/backend/fake-backend.js.map +1 -0
- package/dist/emit/backend/packaging.d.ts +3 -0
- package/dist/emit/backend/packaging.d.ts.map +1 -0
- package/dist/emit/backend/packaging.js +71 -0
- package/dist/emit/backend/packaging.js.map +1 -0
- package/dist/emit/backend/server.d.ts +4 -0
- package/dist/emit/backend/server.d.ts.map +1 -0
- package/dist/emit/backend/server.js +169 -0
- package/dist/emit/backend/server.js.map +1 -0
- package/dist/emit/cli/cli-fake.d.ts +2 -0
- package/dist/emit/cli/cli-fake.d.ts.map +1 -0
- package/dist/emit/cli/cli-fake.js +33 -0
- package/dist/emit/cli/cli-fake.js.map +1 -0
- package/dist/emit/electron/electron-shell.d.ts +3 -0
- package/dist/emit/electron/electron-shell.d.ts.map +1 -0
- package/dist/emit/electron/electron-shell.js +454 -0
- package/dist/emit/electron/electron-shell.js.map +1 -0
- package/dist/emit/engine.d.ts +14 -0
- package/dist/emit/engine.d.ts.map +1 -0
- package/dist/emit/engine.js +25 -0
- package/dist/emit/engine.js.map +1 -0
- package/dist/emit/format.d.ts +2 -0
- package/dist/emit/format.d.ts.map +1 -0
- package/dist/emit/format.js +23 -0
- package/dist/emit/format.js.map +1 -0
- package/dist/emit/frontend/frontend-react.d.ts +4 -0
- package/dist/emit/frontend/frontend-react.d.ts.map +1 -0
- package/dist/emit/frontend/frontend-react.js +2021 -0
- package/dist/emit/frontend/frontend-react.js.map +1 -0
- package/dist/emit/frontend/registry.d.ts +20 -0
- package/dist/emit/frontend/registry.d.ts.map +1 -0
- package/dist/emit/frontend/registry.js +46 -0
- package/dist/emit/frontend/registry.js.map +1 -0
- package/dist/emit/frontend/runtime-emitter.d.ts +4 -0
- package/dist/emit/frontend/runtime-emitter.d.ts.map +1 -0
- package/dist/emit/frontend/runtime-emitter.js +435 -0
- package/dist/emit/frontend/runtime-emitter.js.map +1 -0
- package/dist/emit/frontend/runtime-template.d.ts +28 -0
- package/dist/emit/frontend/runtime-template.d.ts.map +1 -0
- package/dist/emit/frontend/runtime-template.js +218 -0
- package/dist/emit/frontend/runtime-template.js.map +1 -0
- package/dist/emit/frontend/ssg.d.ts +8 -0
- package/dist/emit/frontend/ssg.d.ts.map +1 -0
- package/dist/emit/frontend/ssg.js +219 -0
- package/dist/emit/frontend/ssg.js.map +1 -0
- package/dist/emit/registry.d.ts +17 -0
- package/dist/emit/registry.d.ts.map +1 -0
- package/dist/emit/registry.js +38 -0
- package/dist/emit/registry.js.map +1 -0
- package/dist/emit/static-site/css.d.ts +5 -0
- package/dist/emit/static-site/css.d.ts.map +1 -0
- package/dist/emit/static-site/css.js +872 -0
- package/dist/emit/static-site/css.js.map +1 -0
- package/dist/emit/static-site/enhancements.d.ts +11 -0
- package/dist/emit/static-site/enhancements.d.ts.map +1 -0
- package/dist/emit/static-site/enhancements.js +266 -0
- package/dist/emit/static-site/enhancements.js.map +1 -0
- package/dist/emit/static-site/static-site-html.d.ts +3 -0
- package/dist/emit/static-site/static-site-html.d.ts.map +1 -0
- package/dist/emit/static-site/static-site-html.js +1172 -0
- package/dist/emit/static-site/static-site-html.js.map +1 -0
- package/dist/emit/utils/sdk.d.ts +15 -0
- package/dist/emit/utils/sdk.d.ts.map +1 -0
- package/dist/emit/utils/sdk.js +34 -0
- package/dist/emit/utils/sdk.js.map +1 -0
- package/dist/extensions/context.d.ts +23 -0
- package/dist/extensions/context.d.ts.map +1 -0
- package/dist/extensions/context.js +43 -0
- package/dist/extensions/context.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/decl/backend.raw.schema.d.ts +128 -0
- package/dist/ir/decl/backend.raw.schema.d.ts.map +1 -0
- package/dist/ir/decl/backend.raw.schema.js +24 -0
- package/dist/ir/decl/backend.raw.schema.js.map +1 -0
- package/dist/ir/decl/bundle.d.ts +15 -0
- package/dist/ir/decl/bundle.d.ts.map +1 -0
- package/dist/ir/decl/bundle.js +8 -0
- package/dist/ir/decl/bundle.js.map +1 -0
- package/dist/ir/decl/cli.raw.schema.d.ts +133 -0
- package/dist/ir/decl/cli.raw.schema.d.ts.map +1 -0
- package/dist/ir/decl/cli.raw.schema.js +20 -0
- package/dist/ir/decl/cli.raw.schema.js.map +1 -0
- package/dist/ir/decl/frontend.raw.schema.d.ts +6631 -0
- package/dist/ir/decl/frontend.raw.schema.d.ts.map +1 -0
- package/dist/ir/decl/frontend.raw.schema.js +272 -0
- package/dist/ir/decl/frontend.raw.schema.js.map +1 -0
- package/dist/ir/decl/index.d.ts +6 -0
- package/dist/ir/decl/index.d.ts.map +1 -0
- package/dist/ir/decl/index.js +6 -0
- package/dist/ir/decl/index.js.map +1 -0
- package/dist/ir/decl/normalize.schema.d.ts +9154 -0
- package/dist/ir/decl/normalize.schema.d.ts.map +1 -0
- package/dist/ir/decl/normalize.schema.js +71 -0
- package/dist/ir/decl/normalize.schema.js.map +1 -0
- package/dist/ir/domain/backend.d.ts +19 -0
- package/dist/ir/domain/backend.d.ts.map +1 -0
- package/dist/ir/domain/backend.js +3 -0
- package/dist/ir/domain/backend.js.map +1 -0
- package/dist/ir/domain/cli.d.ts +18 -0
- package/dist/ir/domain/cli.d.ts.map +1 -0
- package/dist/ir/domain/cli.js +2 -0
- package/dist/ir/domain/cli.js.map +1 -0
- package/dist/ir/domain/frontend/index.d.ts +190 -0
- package/dist/ir/domain/frontend/index.d.ts.map +1 -0
- package/dist/ir/domain/frontend/index.js +2 -0
- package/dist/ir/domain/frontend/index.js.map +1 -0
- package/dist/ir/domain/frontend.d.ts +2 -0
- package/dist/ir/domain/frontend.d.ts.map +1 -0
- package/dist/ir/domain/frontend.js +3 -0
- package/dist/ir/domain/frontend.js.map +1 -0
- package/dist/ir/frontend-contract.d.ts +187 -0
- package/dist/ir/frontend-contract.d.ts.map +1 -0
- package/dist/ir/frontend-contract.js +6 -0
- package/dist/ir/frontend-contract.js.map +1 -0
- package/dist/ir/target/backend.d.ts +11 -0
- package/dist/ir/target/backend.d.ts.map +1 -0
- package/dist/ir/target/backend.js +2 -0
- package/dist/ir/target/backend.js.map +1 -0
- package/dist/ir/target/backend.policy.d.ts +896 -0
- package/dist/ir/target/backend.policy.d.ts.map +1 -0
- package/dist/ir/target/backend.policy.js +106 -0
- package/dist/ir/target/backend.policy.js.map +1 -0
- package/dist/ir/target/cli.d.ts +3 -0
- package/dist/ir/target/cli.d.ts.map +1 -0
- package/dist/ir/target/cli.js +2 -0
- package/dist/ir/target/cli.js.map +1 -0
- package/dist/ir/target/electron.d.ts +99 -0
- package/dist/ir/target/electron.d.ts.map +1 -0
- package/dist/ir/target/electron.js +2 -0
- package/dist/ir/target/electron.js.map +1 -0
- package/dist/ir/target/electron.policy.d.ts +7015 -0
- package/dist/ir/target/electron.policy.d.ts.map +1 -0
- package/dist/ir/target/electron.policy.js +119 -0
- package/dist/ir/target/electron.policy.js.map +1 -0
- package/dist/ir/target/frontend.d.ts +12 -0
- package/dist/ir/target/frontend.d.ts.map +1 -0
- package/dist/ir/target/frontend.js +2 -0
- package/dist/ir/target/frontend.js.map +1 -0
- package/dist/ir/target/frontend.policy.d.ts +268 -0
- package/dist/ir/target/frontend.policy.d.ts.map +1 -0
- package/dist/ir/target/frontend.policy.js +33 -0
- package/dist/ir/target/frontend.policy.js.map +1 -0
- package/dist/ir/target/index.d.ts +6 -0
- package/dist/ir/target/index.d.ts.map +1 -0
- package/dist/ir/target/index.js +6 -0
- package/dist/ir/target/index.js.map +1 -0
- package/dist/ir/target/static-site.d.ts +18 -0
- package/dist/ir/target/static-site.d.ts.map +1 -0
- package/dist/ir/target/static-site.js +2 -0
- package/dist/ir/target/static-site.js.map +1 -0
- package/dist/ir/target/static-site.policy.d.ts +2911 -0
- package/dist/ir/target/static-site.policy.d.ts.map +1 -0
- package/dist/ir/target/static-site.policy.js +127 -0
- package/dist/ir/target/static-site.policy.js.map +1 -0
- package/dist/lowering/backend.d.ts +4 -0
- package/dist/lowering/backend.d.ts.map +1 -0
- package/dist/lowering/backend.js +57 -0
- package/dist/lowering/backend.js.map +1 -0
- package/dist/lowering/cli.d.ts +4 -0
- package/dist/lowering/cli.d.ts.map +1 -0
- package/dist/lowering/cli.js +22 -0
- package/dist/lowering/cli.js.map +1 -0
- package/dist/lowering/engine.d.ts +18 -0
- package/dist/lowering/engine.d.ts.map +1 -0
- package/dist/lowering/engine.js +47 -0
- package/dist/lowering/engine.js.map +1 -0
- package/dist/lowering/frontend.d.ts +9 -0
- package/dist/lowering/frontend.d.ts.map +1 -0
- package/dist/lowering/frontend.js +246 -0
- package/dist/lowering/frontend.js.map +1 -0
- package/dist/lowering/targets/to-backend.d.ts +9 -0
- package/dist/lowering/targets/to-backend.d.ts.map +1 -0
- package/dist/lowering/targets/to-backend.js +55 -0
- package/dist/lowering/targets/to-backend.js.map +1 -0
- package/dist/lowering/targets/to-cli.d.ts +4 -0
- package/dist/lowering/targets/to-cli.d.ts.map +1 -0
- package/dist/lowering/targets/to-cli.js +11 -0
- package/dist/lowering/targets/to-cli.js.map +1 -0
- package/dist/lowering/targets/to-electron.d.ts +30 -0
- package/dist/lowering/targets/to-electron.d.ts.map +1 -0
- package/dist/lowering/targets/to-electron.js +87 -0
- package/dist/lowering/targets/to-electron.js.map +1 -0
- package/dist/lowering/targets/to-frontend.d.ts +4 -0
- package/dist/lowering/targets/to-frontend.d.ts.map +1 -0
- package/dist/lowering/targets/to-frontend.js +30 -0
- package/dist/lowering/targets/to-frontend.js.map +1 -0
- package/dist/lowering/targets/to-static-site.d.ts +16 -0
- package/dist/lowering/targets/to-static-site.d.ts.map +1 -0
- package/dist/lowering/targets/to-static-site.js +30 -0
- package/dist/lowering/targets/to-static-site.js.map +1 -0
- package/dist/mappers/index.d.ts +12 -0
- package/dist/mappers/index.d.ts.map +1 -0
- package/dist/mappers/index.js +60 -0
- package/dist/mappers/index.js.map +1 -0
- package/dist/types/extension.d.ts +3 -0
- package/dist/types/extension.d.ts.map +1 -0
- package/dist/types/extension.js +2 -0
- package/dist/types/extension.js.map +1 -0
- package/dist/utils/array.d.ts +2 -0
- package/dist/utils/array.d.ts.map +1 -0
- package/dist/utils/array.js +4 -0
- package/dist/utils/array.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/string.d.ts +13 -0
- package/dist/utils/string.d.ts.map +1 -0
- package/dist/utils/string.js +56 -0
- package/dist/utils/string.js.map +1 -0
- package/package.json +112 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
export function emitRuntime(project, srcDir, ir) {
|
|
4
|
+
const libDir = path.join(srcDir, "lib");
|
|
5
|
+
if (!fs.existsSync(libDir))
|
|
6
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
7
|
+
emitRuntimeContract(project, libDir);
|
|
8
|
+
emitRuntimeImplementation(project, libDir);
|
|
9
|
+
emitRuntimeConfig(project, libDir, ir);
|
|
10
|
+
emitRuntimeHooks(project, libDir);
|
|
11
|
+
}
|
|
12
|
+
function emitRuntimeContract(project, libDir) {
|
|
13
|
+
const filePath = path.join(libDir, "runtime-contract.ts");
|
|
14
|
+
const content = `/**
|
|
15
|
+
* Frontend Core — Runtime Contract
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
19
|
+
export type ResponseType = "json" | "text" | "html" | "blob";
|
|
20
|
+
export type BodyType = "none" | "json" | "text" | "multipart" | "formUrlEncoded";
|
|
21
|
+
|
|
22
|
+
export type Dict<T = unknown> = Record<string, T>;
|
|
23
|
+
export type PathParams = Record<string, string | number>;
|
|
24
|
+
export type QueryParams = Record<string, string | number | boolean | null | undefined>;
|
|
25
|
+
|
|
26
|
+
export type OperationContext =
|
|
27
|
+
| { kind: "page"; pageId: string }
|
|
28
|
+
| { kind: "tableRow"; pageId: string; rowId: string }
|
|
29
|
+
| { kind: "tableBulk"; pageId: string; selectedIds: string[] }
|
|
30
|
+
| { kind: "form"; pageId: string; formId: string }
|
|
31
|
+
| { kind: "system"; reason: string };
|
|
32
|
+
|
|
33
|
+
export type NormalizedErrorCode =
|
|
34
|
+
| "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "VALIDATION_ERROR"
|
|
35
|
+
| "CONFLICT" | "RATE_LIMITED" | "TIMEOUT" | "NETWORK_ERROR"
|
|
36
|
+
| "INTERNAL_ERROR" | "UNKNOWN_ERROR";
|
|
37
|
+
|
|
38
|
+
export interface FieldError { field: string; message: string; code?: string; }
|
|
39
|
+
|
|
40
|
+
export interface NormalizedError {
|
|
41
|
+
code: NormalizedErrorCode;
|
|
42
|
+
message: string;
|
|
43
|
+
status?: number;
|
|
44
|
+
details?: Dict;
|
|
45
|
+
fieldErrors?: FieldError[];
|
|
46
|
+
cause?: unknown;
|
|
47
|
+
raw?: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type AuthState =
|
|
51
|
+
| { status: "anonymous" }
|
|
52
|
+
| { status: "authenticated"; user?: Dict; roles?: string[] }
|
|
53
|
+
| { status: "error"; error: NormalizedError };
|
|
54
|
+
|
|
55
|
+
export type AuthStrategyId = string;
|
|
56
|
+
export interface AuthStrategy {
|
|
57
|
+
id: AuthStrategyId;
|
|
58
|
+
attach(req: RuntimeRequest, ds: DataSourceRuntimeConfig): Promise<RuntimeRequest>;
|
|
59
|
+
loadState?: (rt: Runtime, dsId: string) => Promise<AuthState>;
|
|
60
|
+
onLoginSuccess?: (rt: Runtime, dsId: string, payload: unknown) => Promise<void>;
|
|
61
|
+
onLogout?: (rt: Runtime, dsId: string) => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type CsrfStrategyId = string;
|
|
65
|
+
export interface CsrfStrategy {
|
|
66
|
+
id: CsrfStrategyId;
|
|
67
|
+
attach(req: RuntimeRequest, rt: Runtime, dsId: string): Promise<RuntimeRequest>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DataSourceRuntimeConfig {
|
|
71
|
+
id: string;
|
|
72
|
+
baseUrl: string;
|
|
73
|
+
defaultHeaders?: Record<string, string>;
|
|
74
|
+
withCredentials?: boolean;
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
authStrategyId?: AuthStrategyId;
|
|
77
|
+
csrfStrategyId?: CsrfStrategyId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EnvelopeAdapter {
|
|
81
|
+
id: string;
|
|
82
|
+
extractData: (payload: unknown) => unknown;
|
|
83
|
+
extractMeta?: (payload: unknown) => unknown;
|
|
84
|
+
extractErrorPayload?: (payload: unknown) => unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PaginationResult { total?: number; nextCursor?: string; prevCursor?: string; }
|
|
88
|
+
export interface PaginationAdapter { id: string; extract: (payload: unknown) => PaginationResult; }
|
|
89
|
+
|
|
90
|
+
export interface RequestBodySpec {
|
|
91
|
+
type: BodyType;
|
|
92
|
+
build?: (input: unknown, ctx: OperationContext, rt: Runtime) => Promise<unknown>;
|
|
93
|
+
contentType?: string;
|
|
94
|
+
accept?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ResponseSpec {
|
|
98
|
+
type: ResponseType;
|
|
99
|
+
envelopeAdapterId?: string;
|
|
100
|
+
paginationAdapterId?: string;
|
|
101
|
+
filenameHint?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ResultHandlingSpec {
|
|
105
|
+
invalidate?: Array<{ kind: string; resourceId?: string; id?: string; operationId?: string; key?: any }>;
|
|
106
|
+
redirectTo?: (result: OperationResultNormalized, rt: Runtime) => string | null;
|
|
107
|
+
openUrl?: (result: OperationResultNormalized, rt: Runtime) => string | null;
|
|
108
|
+
downloadAs?: (result: OperationResultNormalized, rt: Runtime) => { filename: string } | null;
|
|
109
|
+
toastOnSuccess?: { kind: string; message: string };
|
|
110
|
+
toastOnError?: { kind: string; message: string };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface OperationSpec {
|
|
114
|
+
id: string;
|
|
115
|
+
datasourceId: string;
|
|
116
|
+
method: HttpMethod;
|
|
117
|
+
path: string;
|
|
118
|
+
pathParams?: (input: unknown, ctx: OperationContext, rt: Runtime) => Promise<PathParams>;
|
|
119
|
+
query?: (input: unknown, ctx: OperationContext, rt: Runtime) => Promise<QueryParams>;
|
|
120
|
+
headers?: (input: unknown, ctx: OperationContext, rt: Runtime) => Promise<Record<string, string>>;
|
|
121
|
+
body?: RequestBodySpec;
|
|
122
|
+
response: ResponseSpec;
|
|
123
|
+
resultHandling?: ResultHandlingSpec;
|
|
124
|
+
requiresAuth?: boolean;
|
|
125
|
+
requiredRoles?: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ResourceSpec {
|
|
129
|
+
id: string;
|
|
130
|
+
datasourceId: string;
|
|
131
|
+
idField?: string;
|
|
132
|
+
listOpId?: string;
|
|
133
|
+
getOpId?: string;
|
|
134
|
+
createOpId?: string;
|
|
135
|
+
updateOpId?: string;
|
|
136
|
+
deleteOpId?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface RuntimeRequest {
|
|
140
|
+
url: string;
|
|
141
|
+
method: HttpMethod;
|
|
142
|
+
headers: Record<string, string>;
|
|
143
|
+
body?: unknown;
|
|
144
|
+
credentials?: RequestCredentials;
|
|
145
|
+
signal?: AbortSignal;
|
|
146
|
+
responseType: ResponseType;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface OperationResultNormalized {
|
|
150
|
+
ok: boolean;
|
|
151
|
+
status?: number;
|
|
152
|
+
data?: unknown;
|
|
153
|
+
meta?: unknown;
|
|
154
|
+
pagination?: PaginationResult;
|
|
155
|
+
error?: NormalizedError;
|
|
156
|
+
raw?: unknown;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface Runtime {
|
|
160
|
+
getDataSource(dsId: string): DataSourceRuntimeConfig;
|
|
161
|
+
getAuthState(dsId: string): AuthState;
|
|
162
|
+
execute(operationId: string, input: unknown, ctx: OperationContext): Promise<OperationResultNormalized>;
|
|
163
|
+
invalidate?(targets: any[]): void;
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
project.createSourceFile(filePath, content, { overwrite: true });
|
|
167
|
+
}
|
|
168
|
+
function emitRuntimeImplementation(project, libDir) {
|
|
169
|
+
const filePath = path.join(libDir, "runtime.ts");
|
|
170
|
+
const content = `import type * as T from "./runtime-contract";
|
|
171
|
+
|
|
172
|
+
export class BaseRuntime implements T.Runtime {
|
|
173
|
+
private authStates: Record<string, T.AuthState> = {};
|
|
174
|
+
|
|
175
|
+
constructor(
|
|
176
|
+
private datasources: T.DataSourceRuntimeConfig[],
|
|
177
|
+
private operations: T.OperationSpec[],
|
|
178
|
+
private adapters: {
|
|
179
|
+
envelope: T.EnvelopeAdapter[];
|
|
180
|
+
pagination: T.PaginationAdapter[];
|
|
181
|
+
},
|
|
182
|
+
private strategies: {
|
|
183
|
+
auth: Record<string, T.AuthStrategy>;
|
|
184
|
+
csrf: Record<string, T.CsrfStrategy>;
|
|
185
|
+
}
|
|
186
|
+
) {}
|
|
187
|
+
|
|
188
|
+
getDataSource(dsId: string): T.DataSourceRuntimeConfig {
|
|
189
|
+
const ds = this.datasources.find((d) => d.id === dsId);
|
|
190
|
+
if (!ds) throw new Error(\`DataSource not found: \${dsId}\`);
|
|
191
|
+
return ds;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getAuthState(dsId: string): T.AuthState {
|
|
195
|
+
return this.authStates[dsId] || { status: "anonymous" };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setAuthState(dsId: string, state: T.AuthState) {
|
|
199
|
+
this.authStates[dsId] = state;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async execute(
|
|
203
|
+
operationId: string,
|
|
204
|
+
input: unknown,
|
|
205
|
+
ctx: T.OperationContext
|
|
206
|
+
): Promise<T.OperationResultNormalized> {
|
|
207
|
+
const op = this.operations.find((o) => o.id === operationId);
|
|
208
|
+
if (!op) throw new Error(\`Operation not found: \${operationId}\`);
|
|
209
|
+
|
|
210
|
+
const ds = this.getDataSource(op.datasourceId);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
let url = \`\${ds.baseUrl}\${op.path}\`;
|
|
214
|
+
|
|
215
|
+
if (op.pathParams) {
|
|
216
|
+
const params = await op.pathParams(input, ctx, this);
|
|
217
|
+
for (const [key, val] of Object.entries(params)) {
|
|
218
|
+
url = url.replace(\`:\${key}\`, String(val));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const queryParams = op.query ? await op.query(input, ctx, this) : {};
|
|
223
|
+
const searchParams = new URLSearchParams();
|
|
224
|
+
for (const [key, val] of Object.entries(queryParams)) {
|
|
225
|
+
if (val !== undefined && val !== null) {
|
|
226
|
+
searchParams.append(key, String(val));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (searchParams.toString()) {
|
|
230
|
+
url += (url.includes("?") ? "&" : "?") + searchParams.toString();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let headers: Record<string, string> = {
|
|
234
|
+
...ds.defaultHeaders,
|
|
235
|
+
...(op.headers ? await op.headers(input, ctx, this) : {}),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let body: any = undefined;
|
|
239
|
+
if (op.body && op.body.type !== "none") {
|
|
240
|
+
body = op.body.build ? await op.body.build(input, ctx, this) : input;
|
|
241
|
+
|
|
242
|
+
if (op.body.type === "json" && typeof body !== "string" && !(body instanceof FormData)) {
|
|
243
|
+
body = JSON.stringify(body);
|
|
244
|
+
headers["Content-Type"] = op.body.contentType || "application/json";
|
|
245
|
+
} else if (op.body.type === "text") {
|
|
246
|
+
headers["Content-Type"] = op.body.contentType || "text/plain";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const req: T.RuntimeRequest = {
|
|
251
|
+
url,
|
|
252
|
+
method: op.method,
|
|
253
|
+
headers,
|
|
254
|
+
body,
|
|
255
|
+
credentials: ds.withCredentials ? "include" : undefined,
|
|
256
|
+
responseType: op.response.type,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
let finalReq = req;
|
|
260
|
+
if (ds.authStrategyId && this.strategies.auth[ds.authStrategyId]) {
|
|
261
|
+
finalReq = await this.strategies.auth[ds.authStrategyId].attach(finalReq, ds);
|
|
262
|
+
}
|
|
263
|
+
if (ds.csrfStrategyId && this.strategies.csrf[ds.csrfStrategyId]) {
|
|
264
|
+
finalReq = await this.strategies.csrf[ds.csrfStrategyId].attach(finalReq, this, ds.id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const response = await fetch(finalReq.url, {
|
|
268
|
+
method: finalReq.method,
|
|
269
|
+
headers: finalReq.headers,
|
|
270
|
+
body: finalReq.body as any,
|
|
271
|
+
credentials: finalReq.credentials,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result: T.OperationResultNormalized = {
|
|
275
|
+
ok: response.ok,
|
|
276
|
+
status: response.status,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
result.error = await this.normalizeError(response);
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let rawPayload: any;
|
|
285
|
+
if (op.response.type === "json") rawPayload = await response.json();
|
|
286
|
+
else if (op.response.type === "blob") rawPayload = await response.blob();
|
|
287
|
+
else rawPayload = await response.text();
|
|
288
|
+
|
|
289
|
+
result.raw = rawPayload;
|
|
290
|
+
|
|
291
|
+
if (op.response.envelopeAdapterId) {
|
|
292
|
+
const adapter = this.adapters.envelope.find(a => a.id === op.response.envelopeAdapterId);
|
|
293
|
+
if (adapter) {
|
|
294
|
+
result.data = adapter.extractData(rawPayload);
|
|
295
|
+
result.meta = adapter.extractMeta?.(rawPayload);
|
|
296
|
+
const err = adapter.extractErrorPayload?.(rawPayload);
|
|
297
|
+
if (err) { result.ok = false; result.error = { code: "UNKNOWN_ERROR", message: String(err), details: err }; }
|
|
298
|
+
} else { result.data = rawPayload; }
|
|
299
|
+
} else { result.data = rawPayload; }
|
|
300
|
+
|
|
301
|
+
if (op.response.paginationAdapterId) {
|
|
302
|
+
const adapter = this.adapters.pagination.find(a => a.id === op.response.paginationAdapterId);
|
|
303
|
+
if (adapter) result.pagination = adapter.extract(rawPayload);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
} catch (err: any) {
|
|
308
|
+
return { ok: false, error: { code: "INTERNAL_ERROR", message: err.message, cause: err } };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private async normalizeError(response: Response): Promise<T.NormalizedError> {
|
|
313
|
+
const status = response.status;
|
|
314
|
+
let code: T.NormalizedErrorCode = "INTERNAL_ERROR";
|
|
315
|
+
if (status === 401) code = "UNAUTHORIZED";
|
|
316
|
+
else if (status === 403) code = "FORBIDDEN";
|
|
317
|
+
else if (status === 404) code = "NOT_FOUND";
|
|
318
|
+
else if (status === 422) code = "VALIDATION_ERROR";
|
|
319
|
+
|
|
320
|
+
let message = response.statusText;
|
|
321
|
+
let details: any = {};
|
|
322
|
+
try {
|
|
323
|
+
const payload = await response.json();
|
|
324
|
+
message = payload.message || message;
|
|
325
|
+
details = payload;
|
|
326
|
+
} catch (_) {}
|
|
327
|
+
|
|
328
|
+
return { code, message, status, details };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
invalidate(targets: any[]): void {
|
|
332
|
+
// hook for UI-level cache invalidation
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
`;
|
|
336
|
+
project.createSourceFile(filePath, content, { overwrite: true });
|
|
337
|
+
}
|
|
338
|
+
function emitRuntimeConfig(project, libDir, ir) {
|
|
339
|
+
const filePath = path.join(libDir, "runtime-instance.ts");
|
|
340
|
+
const serializeLogic = (logic) => {
|
|
341
|
+
if (!logic)
|
|
342
|
+
return "undefined";
|
|
343
|
+
return `async (input: any, ctx: any, rt: any) => (rt as any).evalLogic(${JSON.stringify(logic.logic)}, undefined, { ...input, ...ctx })`;
|
|
344
|
+
};
|
|
345
|
+
const opLines = ir.operations.map(op => ` {
|
|
346
|
+
id: "${op.id}",
|
|
347
|
+
datasourceId: "${op.datasourceId}",
|
|
348
|
+
method: "${op.method}",
|
|
349
|
+
path: "${op.path}",
|
|
350
|
+
response: ${JSON.stringify(op.response)},
|
|
351
|
+
requiresAuth: ${op.requiresAuth ?? false},
|
|
352
|
+
${op.pathParams ? `pathParams: ${serializeLogic(op.pathParams)},` : ""}
|
|
353
|
+
${op.query ? `query: ${serializeLogic(op.query)},` : ""}
|
|
354
|
+
${op.headers ? `headers: ${serializeLogic(op.headers)},` : ""}
|
|
355
|
+
${op.body ? `body: { type: "${op.body.type}", build: ${serializeLogic(op.body.build)} },` : ""}
|
|
356
|
+
${op.resultHandling ? `resultHandling: ${JSON.stringify(op.resultHandling)},` : ""}
|
|
357
|
+
}`);
|
|
358
|
+
const content = `import { BaseRuntime } from "./runtime";
|
|
359
|
+
import * as T from "./runtime-contract";
|
|
360
|
+
|
|
361
|
+
export const datasources: T.DataSourceRuntimeConfig[] = ${JSON.stringify(ir.datasources, null, 2)};
|
|
362
|
+
|
|
363
|
+
export const operations: T.OperationSpec[] = [
|
|
364
|
+
${opLines.join(",\n")}
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
export const resources: T.ResourceSpec[] = ${JSON.stringify(ir.resources, null, 2)};
|
|
368
|
+
|
|
369
|
+
export const envelopeAdapters: T.EnvelopeAdapter[] = [
|
|
370
|
+
{ id: "ok_data_meta", extractData: (p: any) => p?.data, extractMeta: (p: any) => p?.meta, extractErrorPayload: (p: any) => p?.error },
|
|
371
|
+
{ id: "items_cursor", extractData: (p: any) => p?.items, extractMeta: (p: any) => p?.meta, extractErrorPayload: (p: any) => p?.error }
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
export const paginationAdapters: T.PaginationAdapter[] = [
|
|
375
|
+
{ id: "cursor_root", extract: (p: any) => ({ nextCursor: p?.nextCursor }) }
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
export const runtime = new BaseRuntime(
|
|
379
|
+
datasources,
|
|
380
|
+
operations,
|
|
381
|
+
{ envelope: envelopeAdapters, pagination: paginationAdapters },
|
|
382
|
+
{ auth: {}, csrf: {} }
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
(runtime as any).evalLogic = (logic: any, fallback: any, ctx: any) => {
|
|
386
|
+
return logic;
|
|
387
|
+
};
|
|
388
|
+
`;
|
|
389
|
+
project.createSourceFile(filePath, content, { overwrite: true });
|
|
390
|
+
}
|
|
391
|
+
function emitRuntimeHooks(project, libDir) {
|
|
392
|
+
const filePath = path.join(libDir, "hooks.ts");
|
|
393
|
+
const content = `import { useState, useCallback } from "react";
|
|
394
|
+
import { runtime } from "./runtime-instance";
|
|
395
|
+
import * as T from "./runtime-contract";
|
|
396
|
+
|
|
397
|
+
export function useOperation(operationId: string) {
|
|
398
|
+
const [data, setData] = useState<any>(null);
|
|
399
|
+
const [loading, setLoading] = useState(false);
|
|
400
|
+
const [error, setError] = useState<T.NormalizedError | null>(null);
|
|
401
|
+
|
|
402
|
+
const execute = useCallback(async (input: any = {}, ctx: T.OperationContext = { kind: "system", reason: "hook" }) => {
|
|
403
|
+
setLoading(true);
|
|
404
|
+
setError(null);
|
|
405
|
+
try {
|
|
406
|
+
const result = await runtime.execute(operationId, input, ctx);
|
|
407
|
+
if (result.ok) {
|
|
408
|
+
setData(result.data);
|
|
409
|
+
return result;
|
|
410
|
+
} else {
|
|
411
|
+
setError(result.error || { code: "UNKNOWN_ERROR", message: "Failed" } as any);
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
setLoading(false);
|
|
416
|
+
}
|
|
417
|
+
}, [operationId]);
|
|
418
|
+
|
|
419
|
+
return { data, loading, error, execute };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function useResource(resourceId: string) {
|
|
423
|
+
const res = runtime.getDataSource(resourceId); // this is just a placeholder for finding the resource
|
|
424
|
+
return {
|
|
425
|
+
list: useOperation(\`\${resourceId}.list\`),
|
|
426
|
+
get: useOperation(\`\${resourceId}.get\`),
|
|
427
|
+
create: useOperation(\`\${resourceId}.create\`),
|
|
428
|
+
update: useOperation(\`\${resourceId}.update\`),
|
|
429
|
+
delete: useOperation(\`\${resourceId}.delete\`),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
`;
|
|
433
|
+
project.createSourceFile(filePath, content, { overwrite: true });
|
|
434
|
+
}
|
|
435
|
+
//# sourceMappingURL=runtime-emitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-emitter.js","sourceRoot":"","sources":["../../../src/emit/frontend/runtime-emitter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Runtime, OperationSpec, OperationContext, OperationResultNormalized, DataSourceRuntimeConfig, AuthState, EnvelopeAdapter, PaginationAdapter } from "../../ir/frontend-contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* Headless Runtime Implementation (Phase 0 Prototype)
|
|
4
|
+
*/
|
|
5
|
+
export declare class BaseRuntime implements Runtime {
|
|
6
|
+
private datasources;
|
|
7
|
+
private operations;
|
|
8
|
+
private adapters;
|
|
9
|
+
private strategies;
|
|
10
|
+
private authStates;
|
|
11
|
+
constructor(datasources: DataSourceRuntimeConfig[], operations: OperationSpec[], adapters: {
|
|
12
|
+
envelope: EnvelopeAdapter[];
|
|
13
|
+
pagination: PaginationAdapter[];
|
|
14
|
+
}, strategies: {
|
|
15
|
+
auth: any;
|
|
16
|
+
csrf: any;
|
|
17
|
+
});
|
|
18
|
+
getDataSource(dsId: string): DataSourceRuntimeConfig;
|
|
19
|
+
getAuthState(dsId: string): AuthState;
|
|
20
|
+
setAuthState(dsId: string, state: AuthState): void;
|
|
21
|
+
execute(operationId: string, input: unknown, ctx: OperationContext): Promise<OperationResultNormalized>;
|
|
22
|
+
private normalizeError;
|
|
23
|
+
private normalizePayloadError;
|
|
24
|
+
private normalizeException;
|
|
25
|
+
private handleResultSignals;
|
|
26
|
+
invalidate(targets: any): void;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=runtime-template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-template.d.ts","sourceRoot":"","sources":["../../../src/emit/frontend/runtime-template.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,OAAO,EACP,aAAa,EACb,gBAAgB,EAChB,yBAAyB,EACzB,uBAAuB,EACvB,SAAS,EAIT,eAAe,EACf,iBAAiB,EACpB,MAAM,+BAA+B,CAAC;AAEvC;;GAEG;AACH,qBAAa,WAAY,YAAW,OAAO;IAInC,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,UAAU;IATtB,OAAO,CAAC,UAAU,CAAiC;gBAGvC,WAAW,EAAE,uBAAuB,EAAE,EACtC,UAAU,EAAE,aAAa,EAAE,EAC3B,QAAQ,EAAE;QACd,QAAQ,EAAE,eAAe,EAAE,CAAC;QAC5B,UAAU,EAAE,iBAAiB,EAAE,CAAC;KACnC,EACO,UAAU,EAAE;QAChB,IAAI,EAAE,GAAG,CAAC;QACV,IAAI,EAAE,GAAG,CAAC;KACb;IAGL,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB;IAMpD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS;IAIrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS;IAIrC,OAAO,CACT,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,gBAAgB,GACtB,OAAO,CAAC,yBAAyB,CAAC;YAsJvB,cAAc;IAsB5B,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,mBAAmB;IAQ3B,UAAU,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;CAGjC"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Runtime Implementation (Phase 0 Prototype)
|
|
3
|
+
*/
|
|
4
|
+
export class BaseRuntime {
|
|
5
|
+
datasources;
|
|
6
|
+
operations;
|
|
7
|
+
adapters;
|
|
8
|
+
strategies;
|
|
9
|
+
authStates = {};
|
|
10
|
+
constructor(datasources, operations, adapters, strategies) {
|
|
11
|
+
this.datasources = datasources;
|
|
12
|
+
this.operations = operations;
|
|
13
|
+
this.adapters = adapters;
|
|
14
|
+
this.strategies = strategies;
|
|
15
|
+
}
|
|
16
|
+
getDataSource(dsId) {
|
|
17
|
+
const ds = this.datasources.find((d) => d.id === dsId);
|
|
18
|
+
if (!ds)
|
|
19
|
+
throw new Error(`DataSource not found: ${dsId}`);
|
|
20
|
+
return ds;
|
|
21
|
+
}
|
|
22
|
+
getAuthState(dsId) {
|
|
23
|
+
return this.authStates[dsId] || { status: "anonymous" };
|
|
24
|
+
}
|
|
25
|
+
setAuthState(dsId, state) {
|
|
26
|
+
this.authStates[dsId] = state;
|
|
27
|
+
}
|
|
28
|
+
async execute(operationId, input, ctx) {
|
|
29
|
+
const op = this.operations.find((o) => o.id === operationId);
|
|
30
|
+
if (!op)
|
|
31
|
+
throw new Error(`Operation not found: ${operationId}`);
|
|
32
|
+
const ds = this.getDataSource(op.datasourceId);
|
|
33
|
+
try {
|
|
34
|
+
// 1. Build initial request
|
|
35
|
+
let url = `${ds.baseUrl}${op.path}`;
|
|
36
|
+
// Resolve path params
|
|
37
|
+
if (op.pathParams) {
|
|
38
|
+
const params = await op.pathParams(input, ctx, this);
|
|
39
|
+
for (const [key, val] of Object.entries(params)) {
|
|
40
|
+
url = url.replace(`:${key}`, String(val));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Resolve query params
|
|
44
|
+
const queryParams = op.query ? await op.query(input, ctx, this) : {};
|
|
45
|
+
const searchParams = new URLSearchParams();
|
|
46
|
+
for (const [key, val] of Object.entries(queryParams)) {
|
|
47
|
+
if (val !== undefined && val !== null) {
|
|
48
|
+
searchParams.append(key, String(val));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (searchParams.toString()) {
|
|
52
|
+
url += (url.includes("?") ? "&" : "?") + searchParams.toString();
|
|
53
|
+
}
|
|
54
|
+
let headers = {
|
|
55
|
+
...ds.defaultHeaders,
|
|
56
|
+
...(op.headers ? await op.headers(input, ctx, this) : {}),
|
|
57
|
+
};
|
|
58
|
+
let body = undefined;
|
|
59
|
+
if (op.body && op.body.type !== "none") {
|
|
60
|
+
body = op.body.build ? await op.body.build(input, ctx, this) : input;
|
|
61
|
+
if (op.body.type === "json" && typeof body !== "string" && !(body instanceof FormData)) {
|
|
62
|
+
body = JSON.stringify(body);
|
|
63
|
+
headers["Content-Type"] = op.body.contentType || "application/json";
|
|
64
|
+
}
|
|
65
|
+
else if (op.body.type === "text") {
|
|
66
|
+
headers["Content-Type"] = op.body.contentType || "text/plain";
|
|
67
|
+
}
|
|
68
|
+
// Note: multipart (FormData) let fetch set the content-type with boundary
|
|
69
|
+
}
|
|
70
|
+
if (op.response.type === "json") {
|
|
71
|
+
headers["Accept"] = op.response.envelopeAdapterId ? "application/json" : "*/*";
|
|
72
|
+
}
|
|
73
|
+
let req = {
|
|
74
|
+
url,
|
|
75
|
+
method: op.method,
|
|
76
|
+
headers,
|
|
77
|
+
body,
|
|
78
|
+
credentials: ds.withCredentials ? "include" : undefined,
|
|
79
|
+
responseType: op.response.type,
|
|
80
|
+
};
|
|
81
|
+
// 2. Apply Auth Strategy
|
|
82
|
+
if (ds.authStrategyId) {
|
|
83
|
+
const strategy = this.strategies.auth[ds.authStrategyId];
|
|
84
|
+
if (strategy) {
|
|
85
|
+
req = await strategy.attach(req, ds);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 3. Apply CSRF Strategy
|
|
89
|
+
if (ds.csrfStrategyId) {
|
|
90
|
+
const strategy = this.strategies.csrf[ds.csrfStrategyId];
|
|
91
|
+
if (strategy) {
|
|
92
|
+
req = await strategy.attach(req, this, ds.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 4. Perform Fetch
|
|
96
|
+
const response = await fetch(req.url, {
|
|
97
|
+
method: req.method,
|
|
98
|
+
headers: req.headers,
|
|
99
|
+
body: req.body,
|
|
100
|
+
credentials: req.credentials,
|
|
101
|
+
signal: req.signal,
|
|
102
|
+
});
|
|
103
|
+
// 5. Build Result
|
|
104
|
+
const result = {
|
|
105
|
+
ok: response.ok,
|
|
106
|
+
status: response.status,
|
|
107
|
+
};
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
result.error = await this.normalizeError(response);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
// 6. Parse and Normalize Response
|
|
113
|
+
let rawPayload;
|
|
114
|
+
if (op.response.type === "json") {
|
|
115
|
+
rawPayload = await response.json();
|
|
116
|
+
}
|
|
117
|
+
else if (op.response.type === "text" || op.response.type === "html") {
|
|
118
|
+
rawPayload = await response.text();
|
|
119
|
+
}
|
|
120
|
+
else if (op.response.type === "blob") {
|
|
121
|
+
rawPayload = await response.blob();
|
|
122
|
+
}
|
|
123
|
+
result.raw = rawPayload;
|
|
124
|
+
// Apply Envelope Adapter
|
|
125
|
+
if (op.response.envelopeAdapterId) {
|
|
126
|
+
const adapter = this.adapters.envelope.find((a) => a.id === op.response.envelopeAdapterId);
|
|
127
|
+
if (adapter) {
|
|
128
|
+
result.data = adapter.extractData(rawPayload);
|
|
129
|
+
result.meta = adapter.extractMeta?.(rawPayload);
|
|
130
|
+
const errorPayload = adapter.extractErrorPayload?.(rawPayload);
|
|
131
|
+
if (errorPayload) {
|
|
132
|
+
result.ok = false;
|
|
133
|
+
result.error = this.normalizePayloadError(errorPayload, response.status);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
result.data = rawPayload;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
result.data = rawPayload;
|
|
142
|
+
}
|
|
143
|
+
// Apply Pagination Adapter
|
|
144
|
+
if (op.response.paginationAdapterId) {
|
|
145
|
+
const adapter = this.adapters.pagination.find((a) => a.id === op.response.paginationAdapterId);
|
|
146
|
+
if (adapter) {
|
|
147
|
+
result.pagination = adapter.extract(rawPayload);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 7. Handle Outcomes (Toasts, Invalidations, etc. - usually delegated to UI hooks but runtime can signal)
|
|
151
|
+
if (result.ok && op.resultHandling) {
|
|
152
|
+
this.handleResultSignals(op.resultHandling, result);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: this.normalizeException(err),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async normalizeError(response) {
|
|
164
|
+
const status = response.status;
|
|
165
|
+
let code = "INTERNAL_ERROR";
|
|
166
|
+
let message = response.statusText;
|
|
167
|
+
if (status === 401)
|
|
168
|
+
code = "UNAUTHORIZED";
|
|
169
|
+
else if (status === 403)
|
|
170
|
+
code = "FORBIDDEN";
|
|
171
|
+
else if (status === 404)
|
|
172
|
+
code = "NOT_FOUND";
|
|
173
|
+
else if (status === 422)
|
|
174
|
+
code = "VALIDATION_ERROR";
|
|
175
|
+
else if (status === 409)
|
|
176
|
+
code = "CONFLICT";
|
|
177
|
+
else if (status === 429)
|
|
178
|
+
code = "RATE_LIMITED";
|
|
179
|
+
let details = {};
|
|
180
|
+
try {
|
|
181
|
+
const payload = await response.json();
|
|
182
|
+
message = payload.message || message;
|
|
183
|
+
details = payload;
|
|
184
|
+
}
|
|
185
|
+
catch (_) { }
|
|
186
|
+
return { code, message, status, details };
|
|
187
|
+
}
|
|
188
|
+
normalizePayloadError(payload, status) {
|
|
189
|
+
return {
|
|
190
|
+
code: "UNKNOWN_ERROR",
|
|
191
|
+
message: typeof payload === "string" ? payload : (payload.message || "Operation failed"),
|
|
192
|
+
details: payload,
|
|
193
|
+
status,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
normalizeException(err) {
|
|
197
|
+
if (err.name === "AbortError")
|
|
198
|
+
return { code: "TIMEOUT", message: "Request timed out" };
|
|
199
|
+
if (err instanceof TypeError)
|
|
200
|
+
return { code: "NETWORK_ERROR", message: "Network error or CORS issue" };
|
|
201
|
+
return {
|
|
202
|
+
code: "INTERNAL_ERROR",
|
|
203
|
+
message: err.message || "An unexpected error occurred",
|
|
204
|
+
cause: err,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
handleResultSignals(spec, result) {
|
|
208
|
+
// In a real implementation, this would trigger event emitters or state updates
|
|
209
|
+
// that the UI hooks (useOperation) listen to.
|
|
210
|
+
if (spec.invalidate && typeof this.invalidate === "function") {
|
|
211
|
+
this.invalidate(spec.invalidate);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
invalidate(targets) {
|
|
215
|
+
// Implementation for cache invalidation (e.g. TanStack Query integration)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=runtime-template.js.map
|