universal-api-layer 0.7.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 +82 -0
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/SECURITY.md +9 -0
- package/dist/angular/index.d.ts +62 -0
- package/dist/angular/index.js +142 -0
- package/dist/chunk-DXYWZQW5.js +92 -0
- package/dist/chunk-HDW7BOZI.js +764 -0
- package/dist/chunk-YTM3FV65.js +19 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +205 -0
- package/dist/client-ByBlZaRg.d.ts +169 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +26 -0
- package/dist/react/index.d.ts +16 -0
- package/dist/react/index.js +227 -0
- package/dist/types-CVzD7l4p.d.ts +81 -0
- package/dist/vue/index.d.ts +23 -0
- package/dist/vue/index.js +257 -0
- package/dist/websocket/angular.d.ts +9 -0
- package/dist/websocket/angular.js +15 -0
- package/dist/websocket/index.d.ts +7 -0
- package/dist/websocket/index.js +529 -0
- package/dist/websocket/react.d.ts +8 -0
- package/dist/websocket/react.js +23 -0
- package/dist/websocket/vue.d.ts +9 -0
- package/dist/websocket/vue.js +39 -0
- package/package.json +181 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var ApiError = class extends Error {
|
|
3
|
+
constructor(message, status = 0, fieldErrors = {}, data, code = "http", cause) {
|
|
4
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.fieldErrors = fieldErrors;
|
|
7
|
+
this.data = data;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "ApiError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
fieldErrors;
|
|
13
|
+
data;
|
|
14
|
+
code;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
ApiError
|
|
19
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { createInterface } from "readline/promises";
|
|
7
|
+
function templates(framework, language) {
|
|
8
|
+
if (framework === "angular") {
|
|
9
|
+
if (language !== "typescript")
|
|
10
|
+
throw new Error("Angular scaffolding requires TypeScript.");
|
|
11
|
+
return {
|
|
12
|
+
"src/api.providers.ts": `import {createApiClient} from 'universal-api-layer';
|
|
13
|
+
import {provideApiClient} from 'universal-api-layer/angular';
|
|
14
|
+
export const apiProviders=provideApiClient(()=>createApiClient({baseUrl:'/api'}));
|
|
15
|
+
`,
|
|
16
|
+
"src/ApiExample.ts": `import {Component,DestroyRef,inject,signal} from '@angular/core';
|
|
17
|
+
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
|
18
|
+
import {ApiService} from 'universal-api-layer/angular';
|
|
19
|
+
@Component({selector:'api-example',standalone:true,template:'<button (click)="load()">Load users</button><pre>{{ data() }}</pre><p role="alert">{{ error() }}</p>'})
|
|
20
|
+
export class ApiExample {private api=inject(ApiService);private destroy=inject(DestroyRef);readonly data=signal('');readonly error=signal('');load(){this.api.request$('/users').pipe(takeUntilDestroyed(this.destroy)).subscribe({next:value=>this.data.set(JSON.stringify(value)),error:cause=>this.error.set(cause instanceof Error?cause.message:'Request failed')});}}
|
|
21
|
+
`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const ts = language === "typescript";
|
|
25
|
+
const ext = ts ? "ts" : "js";
|
|
26
|
+
const setup = `import { createApiClient } from 'universal-api-layer';
|
|
27
|
+
export const client = createApiClient({ baseUrl: '/api' });
|
|
28
|
+
`;
|
|
29
|
+
const type = ts ? "<{ data: { id: number; name: string }[] }, { id: number; name: string }[]>" : "";
|
|
30
|
+
if (framework === "react")
|
|
31
|
+
return {
|
|
32
|
+
[`src/api.${ext}`]: setup,
|
|
33
|
+
[`src/ApiExample.${ts ? "tsx" : "jsx"}`]: `import { useApiQuery, useApiMutation } from 'universal-api-layer/react';
|
|
34
|
+
export default function ApiExample() {
|
|
35
|
+
const users = useApiQuery${type}({ key: ['users'], url: '/users', select: response => response?.data ?? [] });
|
|
36
|
+
const create = useApiMutation({ url: '/users', invalidateKeys: [['users']] });
|
|
37
|
+
return <section><button onClick={() => create.mutate({ name: 'Bahaa' })}>Add user</button>{users.isPending ? <p>Loading...</p> : users.error ? <p>{users.error.message}</p> : <pre>{JSON.stringify(users.data)}</pre>}{create.error && <pre>{JSON.stringify(create.error.fieldErrors)}</pre>}</section>;
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
[`src/api.${ext}`]: setup,
|
|
43
|
+
"src/ApiExample.vue": `<script setup${ts ? ' lang="ts"' : ""}>
|
|
44
|
+
import { useApiQuery, useApiMutation } from 'universal-api-layer/vue';
|
|
45
|
+
const { data, isPending, error } = useApiQuery${type}({ key: ['users'], url: '/users', select: response => response?.data ?? [] });
|
|
46
|
+
const { mutate, error: mutationError } = useApiMutation({ url: '/users', invalidateKeys: [['users']] });
|
|
47
|
+
</script>
|
|
48
|
+
<template><section><button @click="mutate({ name: 'Bahaa' })">Add user</button><p v-if="isPending">Loading...</p><p v-else-if="error">{{ error.message }}</p><pre v-else>{{ data }}</pre><pre v-if="mutationError">{{ mutationError.fieldErrors }}</pre></section></template>
|
|
49
|
+
`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function main() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
if (args.includes("--help")) {
|
|
55
|
+
console.log(
|
|
56
|
+
"universal-api-layer init [--framework react|vue|angular] [--language javascript|typescript] [--force]"
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (args.shift() !== "init")
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Use: universal-api-layer init --framework react|vue|angular --language javascript|typescript"
|
|
63
|
+
);
|
|
64
|
+
const flags = {};
|
|
65
|
+
let force = false;
|
|
66
|
+
while (args.length) {
|
|
67
|
+
const arg = args.shift();
|
|
68
|
+
if (arg === "--force") {
|
|
69
|
+
force = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!["--framework", "--language"].includes(arg) || !args[0] || args[0].startsWith("--"))
|
|
73
|
+
throw new Error(`Invalid option ${arg}. Use --help.`);
|
|
74
|
+
flags[arg.slice(2)] = args.shift();
|
|
75
|
+
}
|
|
76
|
+
if (flags.framework && !["react", "vue", "angular"].includes(flags.framework))
|
|
77
|
+
throw new Error("Expected --framework react|vue|angular.");
|
|
78
|
+
if (flags.language && !["javascript", "typescript"].includes(flags.language))
|
|
79
|
+
throw new Error("Expected --language javascript|typescript.");
|
|
80
|
+
let pkg = {};
|
|
81
|
+
try {
|
|
82
|
+
pkg = JSON.parse(await readFile("package.json", "utf8"));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error.code !== "ENOENT") throw error;
|
|
85
|
+
}
|
|
86
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
87
|
+
const frameworks = ["react", "vue", "angular"].filter(
|
|
88
|
+
(name) => deps[name === "angular" ? "@angular/core" : name]
|
|
89
|
+
);
|
|
90
|
+
const exists = async (name) => access(name).then(
|
|
91
|
+
() => true,
|
|
92
|
+
() => false
|
|
93
|
+
);
|
|
94
|
+
let framework = flags.framework ?? (frameworks.length === 1 ? frameworks[0] : void 0);
|
|
95
|
+
const hasTsConfig = await exists("tsconfig.json");
|
|
96
|
+
const hasJsConfig = await exists("jsconfig.json");
|
|
97
|
+
let language = flags.language ?? (hasTsConfig !== hasJsConfig ? hasTsConfig ? "typescript" : "javascript" : void 0);
|
|
98
|
+
if (framework === "angular") language = flags.language ?? "typescript";
|
|
99
|
+
if (!language && !(hasTsConfig && hasJsConfig)) {
|
|
100
|
+
const { readdir } = await import("fs/promises");
|
|
101
|
+
const ignored = /* @__PURE__ */ new Set([
|
|
102
|
+
"node_modules",
|
|
103
|
+
"dist",
|
|
104
|
+
"build",
|
|
105
|
+
"coverage",
|
|
106
|
+
".git",
|
|
107
|
+
".next",
|
|
108
|
+
".nuxt",
|
|
109
|
+
".output",
|
|
110
|
+
".artifacts",
|
|
111
|
+
"tests",
|
|
112
|
+
"test"
|
|
113
|
+
]);
|
|
114
|
+
async function sourceFiles(directory) {
|
|
115
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
116
|
+
const children = await Promise.all(
|
|
117
|
+
entries.filter(
|
|
118
|
+
(entry) => !ignored.has(entry.name) && !entry.name.startsWith(".")
|
|
119
|
+
).map(async (entry) => {
|
|
120
|
+
const name = resolve(directory, entry.name);
|
|
121
|
+
if (entry.isDirectory()) return sourceFiles(name);
|
|
122
|
+
return entry.isFile() && /\.(?:[cm]?[jt]sx?|vue)$/.test(entry.name) && !/\.(?:config|test|spec|d)\.[cm]?[jt]sx?$/.test(entry.name) ? [name] : [];
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
return children.flat();
|
|
126
|
+
}
|
|
127
|
+
const files2 = await sourceFiles(await exists("src") ? "src" : ".");
|
|
128
|
+
let ts = files2.some((f) => /\.(ts|tsx|mts|cts)$/.test(f));
|
|
129
|
+
let js = files2.some((f) => /\.(js|jsx|mjs|cjs)$/.test(f));
|
|
130
|
+
for (const name of files2.filter((f) => f.endsWith(".vue"))) {
|
|
131
|
+
const source = await readFile(name, "utf8");
|
|
132
|
+
if (/<script\b[^>]*\blang\s*=\s*['"]ts['"]/i.test(source)) ts = true;
|
|
133
|
+
else js = true;
|
|
134
|
+
}
|
|
135
|
+
if (ts !== js) language = ts ? "typescript" : "javascript";
|
|
136
|
+
}
|
|
137
|
+
async function choose(label, choices) {
|
|
138
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Cannot detect ${label}. Pass --${label} ${choices.join("|")} in non-interactive mode.`
|
|
141
|
+
);
|
|
142
|
+
const input = createInterface({
|
|
143
|
+
input: process.stdin,
|
|
144
|
+
output: process.stdout
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
return (await input.question(`${label} (${choices.join("/")}): `)).trim();
|
|
148
|
+
} finally {
|
|
149
|
+
input.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
framework ??= await choose("framework", ["react", "vue", "angular"]);
|
|
153
|
+
if (framework === "angular") {
|
|
154
|
+
if (flags.language === "javascript")
|
|
155
|
+
throw new Error("Angular scaffolding requires TypeScript.");
|
|
156
|
+
language = "typescript";
|
|
157
|
+
}
|
|
158
|
+
language ??= await choose("language", ["javascript", "typescript"]);
|
|
159
|
+
if (!["react", "vue", "angular"].includes(framework) || !["javascript", "typescript"].includes(language))
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Expected --framework react|vue|angular and --language javascript|typescript."
|
|
162
|
+
);
|
|
163
|
+
const files = templates(framework, language);
|
|
164
|
+
if (!force) {
|
|
165
|
+
for (const name of Object.keys(files))
|
|
166
|
+
if (await exists(name))
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Refusing to overwrite ${name}. Use --force to replace generated files.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
for (const [name, content] of Object.entries(files)) {
|
|
172
|
+
await mkdir(resolve(name, ".."), { recursive: true });
|
|
173
|
+
await writeFile(name, content, { flag: force ? "w" : "wx" });
|
|
174
|
+
}
|
|
175
|
+
if (framework === "angular") {
|
|
176
|
+
console.log(
|
|
177
|
+
"Created Angular API providers and standalone example. Add apiProviders from './api.providers' to your application configuration providers, then import ApiExample into a standalone component."
|
|
178
|
+
);
|
|
179
|
+
console.log(
|
|
180
|
+
"Install: npm install universal-api-layer @angular/core rxjs@^7.8.2\nUse Angular 20.3-22 and its matching Angular/Node/TypeScript toolchain. No existing bootstrap files were changed."
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log(
|
|
185
|
+
`Created API setup and example.
|
|
186
|
+
Install: npm install universal-api-layer ${framework === "react" ? "react@^19 react-dom@^19 @tanstack/react-query@^5" : "vue@^3.5 @tanstack/vue-query@^5"}`
|
|
187
|
+
);
|
|
188
|
+
console.log(
|
|
189
|
+
"Requires Node >=22.13 and npm >=11.21.1; install the listed optional peers explicitly."
|
|
190
|
+
);
|
|
191
|
+
console.log(
|
|
192
|
+
framework === "react" ? "Import { client } from './api'; import { ApiProvider } from 'universal-api-layer/react'; wrap <App /> with <ApiProvider client={client}>. ApiProvider installs QueryClientProvider; pass queryClient to reuse an existing instance." : "Import { client } from './api'; import { createApiPlugin } from 'universal-api-layer/vue'; call app.use(createApiPlugin({ client })) before app.mount()."
|
|
193
|
+
);
|
|
194
|
+
if (deps.next || deps.nuxt)
|
|
195
|
+
console.log(
|
|
196
|
+
"Next.js/Nuxt detected: only standalone API/example files were generated; register them manually. Next.js requires a client provider boundary; Nuxt requires a client plugin and a per-request client for SSR. Configure these manually."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
main().catch((error) => {
|
|
200
|
+
console.error(error.message);
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
});
|
|
203
|
+
export {
|
|
204
|
+
templates
|
|
205
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
type FieldErrors = Record<string, string[]>;
|
|
2
|
+
type ApiErrorCode = "backpressure" | "http" | "network" | "timeout" | "cancelled" | "auth" | "parse" | "configuration" | "callback" | "validation";
|
|
3
|
+
declare class ApiError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
fieldErrors: FieldErrors;
|
|
6
|
+
data?: unknown | undefined;
|
|
7
|
+
code: ApiErrorCode;
|
|
8
|
+
constructor(message: string, status?: number, fieldErrors?: FieldErrors, data?: unknown | undefined, code?: ApiErrorCode, cause?: unknown);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ApiSession {
|
|
12
|
+
userId: string;
|
|
13
|
+
token?: string | null;
|
|
14
|
+
tenantId?: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface SessionSnapshot {
|
|
17
|
+
readonly userId: string | null;
|
|
18
|
+
readonly tenantId: string | null;
|
|
19
|
+
readonly revision: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Structural Standard Schema v1 support: Zod, Valibot and other compatible schemas need no runtime dependency here. */
|
|
23
|
+
interface StandardSchema<T> {
|
|
24
|
+
readonly "~standard": {
|
|
25
|
+
readonly version: 1;
|
|
26
|
+
readonly vendor: string;
|
|
27
|
+
readonly validate: (value: unknown) => {
|
|
28
|
+
readonly value: T;
|
|
29
|
+
readonly issues?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
readonly issues: readonly {
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}[];
|
|
34
|
+
} | Promise<{
|
|
35
|
+
readonly value: T;
|
|
36
|
+
readonly issues?: undefined;
|
|
37
|
+
} | {
|
|
38
|
+
readonly issues: readonly {
|
|
39
|
+
readonly message: string;
|
|
40
|
+
}[];
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
type ResponseSchema<T> = ((value: unknown) => T | Promise<T>) | {
|
|
45
|
+
parse(value: unknown): T;
|
|
46
|
+
parseAsync?: (value: unknown) => Promise<T>;
|
|
47
|
+
} | StandardSchema<T>;
|
|
48
|
+
|
|
49
|
+
type Params = Record<string, string | number | boolean | null | undefined | readonly (string | number | boolean)[]>;
|
|
50
|
+
/** Uses each platform's native Fetch types without requiring DOM libs in Node projects. */
|
|
51
|
+
type ApiHeaders = NonNullable<RequestInit["headers"]>;
|
|
52
|
+
interface ApiResponse<T = unknown> {
|
|
53
|
+
readonly data: T | null;
|
|
54
|
+
readonly headers: Headers;
|
|
55
|
+
readonly status: number;
|
|
56
|
+
readonly statusText: string;
|
|
57
|
+
readonly url: string;
|
|
58
|
+
}
|
|
59
|
+
interface RequestOptions<T = unknown> {
|
|
60
|
+
/** Keep body-only results by default; full mode also exposes response metadata. */
|
|
61
|
+
responseMode?: "data" | "full";
|
|
62
|
+
schema?: ResponseSchema<T> | false;
|
|
63
|
+
authRetry?: boolean;
|
|
64
|
+
requiresAuth?: boolean;
|
|
65
|
+
method?: string;
|
|
66
|
+
params?: Params;
|
|
67
|
+
body?: unknown;
|
|
68
|
+
headers?: ApiHeaders;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
}
|
|
72
|
+
interface StreamOptions extends Omit<RequestOptions, "responseMode" | "schema"> {
|
|
73
|
+
/** text yields decoded UTF-8 chunks; sse yields complete SSE events. */
|
|
74
|
+
format?: "text" | "sse";
|
|
75
|
+
/** Maximum buffered SSE line/event length in UTF-16 code units. Default 1 MiB. */
|
|
76
|
+
maxEventSize?: number;
|
|
77
|
+
}
|
|
78
|
+
interface ServerSentEvent {
|
|
79
|
+
readonly event: string;
|
|
80
|
+
readonly data: string;
|
|
81
|
+
readonly id: string;
|
|
82
|
+
/** Server retry hint only; this client never reconnects automatically. */
|
|
83
|
+
readonly retry?: number;
|
|
84
|
+
}
|
|
85
|
+
interface ClientOptions {
|
|
86
|
+
baseUrl?: string;
|
|
87
|
+
session?: ApiSession | null;
|
|
88
|
+
/** Return a new access token. Shared across concurrent 401s and retried once. */
|
|
89
|
+
refreshToken?: (signal: AbortSignal, session: SessionSnapshot) => Promise<string>;
|
|
90
|
+
refreshTimeoutMs?: number;
|
|
91
|
+
schema?: ResponseSchema<unknown>;
|
|
92
|
+
getToken?: (signal: AbortSignal) => string | null | undefined | Promise<string | null | undefined>;
|
|
93
|
+
headers?: ApiHeaders;
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
fetch?: typeof fetch;
|
|
96
|
+
credentials?: RequestInit["credentials"];
|
|
97
|
+
normalizeErrors?: (data: unknown) => FieldErrors;
|
|
98
|
+
/** Non-secret stable identity for intentional cache sharing/SSR hydration. Unique per client by default. */
|
|
99
|
+
cacheScope?: string;
|
|
100
|
+
/** Additional HTTP(S) origins allowed to receive this client's requests and credentials. */
|
|
101
|
+
allowedOrigins?: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
interface QueryConfig<T, S = T | null> {
|
|
104
|
+
schema?: ResponseSchema<T> | false;
|
|
105
|
+
requiresAuth?: boolean;
|
|
106
|
+
authRetry?: boolean;
|
|
107
|
+
key: readonly unknown[];
|
|
108
|
+
url: string;
|
|
109
|
+
params?: Params;
|
|
110
|
+
headers?: ApiHeaders;
|
|
111
|
+
/** Non-secret account/tenant identity; change on authentication transitions. */
|
|
112
|
+
cacheScope?: string;
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
enabled?: boolean;
|
|
115
|
+
staleTime?: number;
|
|
116
|
+
retry?: boolean | number;
|
|
117
|
+
select?: (response: T | null) => S;
|
|
118
|
+
}
|
|
119
|
+
interface MutationConfig<T, V, S = T | null> {
|
|
120
|
+
schema?: ResponseSchema<T> | false;
|
|
121
|
+
requiresAuth?: boolean;
|
|
122
|
+
authRetry?: boolean;
|
|
123
|
+
url: string | ((variables: V) => string);
|
|
124
|
+
method?: string;
|
|
125
|
+
mapPayload?: (variables: V) => unknown;
|
|
126
|
+
headers?: ApiHeaders;
|
|
127
|
+
timeoutMs?: number;
|
|
128
|
+
signal?: AbortSignal;
|
|
129
|
+
select?: (response: T | null) => S;
|
|
130
|
+
invalidateKeys?: readonly (readonly unknown[])[];
|
|
131
|
+
/** all explicitly includes matching native TanStack queries in this QueryClient. */
|
|
132
|
+
invalidateScope?: "client" | "all";
|
|
133
|
+
onSuccess?: (data: S, variables: V) => unknown | Promise<unknown>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
137
|
+
declare function createApiClient(config?: ClientOptions): Readonly<{
|
|
138
|
+
cacheScope: string;
|
|
139
|
+
queryKey: (key: readonly unknown[], url: string, params?: Params, headers?: ApiHeaders, scope?: string) => unknown[];
|
|
140
|
+
setSession: (value: ApiSession | null) => void;
|
|
141
|
+
logout: () => void;
|
|
142
|
+
getSessionSnapshot: () => SessionSnapshot;
|
|
143
|
+
subscribeSession: (listener: () => void) => () => void;
|
|
144
|
+
assertSession: (revision: number) => void;
|
|
145
|
+
withSession<T>(operation: () => T | Promise<T>, revision?: number): Promise<T>;
|
|
146
|
+
ownsQuery(key: readonly unknown[]): boolean;
|
|
147
|
+
request: {
|
|
148
|
+
<T = unknown>(url: string, request: RequestOptions<T> & {
|
|
149
|
+
responseMode: "full";
|
|
150
|
+
}): Promise<ApiResponse<T>>;
|
|
151
|
+
<T = unknown>(url: string, request?: RequestOptions<T> & {
|
|
152
|
+
responseMode?: "data";
|
|
153
|
+
}): Promise<T | null>;
|
|
154
|
+
<T = unknown>(url: string, request: RequestOptions<T>): Promise<T | null | ApiResponse<T>>;
|
|
155
|
+
};
|
|
156
|
+
stream: {
|
|
157
|
+
(url: string, request: StreamOptions & {
|
|
158
|
+
format: "sse";
|
|
159
|
+
}): AsyncGenerator<ServerSentEvent, void, unknown>;
|
|
160
|
+
(url: string, request?: StreamOptions & {
|
|
161
|
+
format?: "text";
|
|
162
|
+
}): AsyncGenerator<string, void, unknown>;
|
|
163
|
+
(url: string, request: StreamOptions): AsyncGenerator<string | ServerSentEvent, void, unknown>;
|
|
164
|
+
};
|
|
165
|
+
}>;
|
|
166
|
+
type ApiClient = ReturnType<typeof createApiClient>;
|
|
167
|
+
|
|
168
|
+
export { DEFAULT_TIMEOUT_MS as D, ApiError as f, createApiClient as j };
|
|
169
|
+
export type { ApiHeaders as A, ClientOptions as C, FieldErrors as F, MutationConfig as M, Params as P, QueryConfig as Q, RequestOptions as R, SessionSnapshot as S, ApiSession as a, ApiResponse as b, StreamOptions as c, ServerSentEvent as d, ApiClient as e, ApiErrorCode as g, ResponseSchema as h, StandardSchema as i };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { f as ApiError, F as FieldErrors, e as ApiClient, P as Params, A as ApiHeaders, M as MutationConfig } from '../client-ByBlZaRg.js';
|
|
2
|
+
export { g as ApiErrorCode, b as ApiResponse, a as ApiSession, C as ClientOptions, D as DEFAULT_TIMEOUT_MS, Q as QueryConfig, R as RequestOptions, h as ResponseSchema, d as ServerSentEvent, S as SessionSnapshot, i as StandardSchema, c as StreamOptions, j as createApiClient } from '../client-ByBlZaRg.js';
|
|
3
|
+
|
|
4
|
+
declare function invokeCallback<T>(callback: () => T): T;
|
|
5
|
+
declare function invokeAsyncCallback(callback: () => unknown | Promise<unknown>): Promise<void>;
|
|
6
|
+
|
|
7
|
+
declare function laravelErrors(data: unknown): FieldErrors;
|
|
8
|
+
declare function queryIdentity(client: ApiClient, key: readonly unknown[], url: string, params?: Params, headers?: ApiHeaders, scope?: string): unknown[];
|
|
9
|
+
declare function runMutation<T, V, S = T | null>(client: ApiClient, config: MutationConfig<T, V, S>, variables: V): Promise<S>;
|
|
10
|
+
/** Avoid retries for deterministic schema/auth/client failures; honor adapter or QueryClient overrides. */
|
|
11
|
+
declare function defaultQueryRetry(failureCount: number, error: ApiError): boolean;
|
|
12
|
+
|
|
13
|
+
export { ApiClient, ApiError, ApiHeaders, FieldErrors, MutationConfig, Params, defaultQueryRetry, invokeAsyncCallback, invokeCallback, laravelErrors, queryIdentity, runMutation };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TIMEOUT_MS,
|
|
3
|
+
createApiClient,
|
|
4
|
+
defaultQueryRetry,
|
|
5
|
+
laravelErrors,
|
|
6
|
+
queryIdentity,
|
|
7
|
+
runMutation
|
|
8
|
+
} from "../chunk-HDW7BOZI.js";
|
|
9
|
+
import {
|
|
10
|
+
invokeAsyncCallback,
|
|
11
|
+
invokeCallback
|
|
12
|
+
} from "../chunk-DXYWZQW5.js";
|
|
13
|
+
import {
|
|
14
|
+
ApiError
|
|
15
|
+
} from "../chunk-YTM3FV65.js";
|
|
16
|
+
export {
|
|
17
|
+
ApiError,
|
|
18
|
+
DEFAULT_TIMEOUT_MS,
|
|
19
|
+
createApiClient,
|
|
20
|
+
defaultQueryRetry,
|
|
21
|
+
invokeAsyncCallback,
|
|
22
|
+
invokeCallback,
|
|
23
|
+
laravelErrors,
|
|
24
|
+
queryIdentity,
|
|
25
|
+
runMutation
|
|
26
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { QueryClient, UseMutationResult, UseQueryResult } from '@tanstack/react-query';
|
|
4
|
+
import { ApiClient, MutationConfig, ApiError, QueryConfig, SessionSnapshot } from '../core/index.js';
|
|
5
|
+
|
|
6
|
+
declare function ApiProvider({ client, queryClient, children, }: {
|
|
7
|
+
client: ApiClient;
|
|
8
|
+
queryClient?: QueryClient;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): react.JSX.Element;
|
|
11
|
+
declare function useApiClient(): ApiClient;
|
|
12
|
+
declare function useApiSession(): SessionSnapshot;
|
|
13
|
+
declare function useApiQuery<T = unknown, S = T | null>(config: QueryConfig<T, S>): UseQueryResult<S, ApiError>;
|
|
14
|
+
declare function useApiMutation<T = unknown, V = unknown, S = T | null>(config: MutationConfig<T, V, S>): UseMutationResult<S, ApiError, V, unknown>;
|
|
15
|
+
|
|
16
|
+
export { ApiProvider, useApiClient, useApiMutation, useApiQuery, useApiSession };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultQueryRetry,
|
|
3
|
+
queryIdentity,
|
|
4
|
+
runMutation
|
|
5
|
+
} from "../chunk-HDW7BOZI.js";
|
|
6
|
+
import {
|
|
7
|
+
invokeAsyncCallback,
|
|
8
|
+
invokeCallback
|
|
9
|
+
} from "../chunk-DXYWZQW5.js";
|
|
10
|
+
import "../chunk-YTM3FV65.js";
|
|
11
|
+
|
|
12
|
+
// src/react/index.tsx
|
|
13
|
+
import {
|
|
14
|
+
createContext,
|
|
15
|
+
useContext,
|
|
16
|
+
useEffect,
|
|
17
|
+
useMemo,
|
|
18
|
+
useState,
|
|
19
|
+
useSyncExternalStore
|
|
20
|
+
} from "react";
|
|
21
|
+
import {
|
|
22
|
+
QueryClient,
|
|
23
|
+
QueryClientProvider,
|
|
24
|
+
useQuery,
|
|
25
|
+
useMutation,
|
|
26
|
+
useQueryClient
|
|
27
|
+
} from "@tanstack/react-query";
|
|
28
|
+
import { jsx } from "react/jsx-runtime";
|
|
29
|
+
var Context = createContext(null);
|
|
30
|
+
function clearClientCache(client, cache, staleOnly = false) {
|
|
31
|
+
const current = client.getSessionSnapshot();
|
|
32
|
+
const predicate = (query) => {
|
|
33
|
+
if (!client.ownsQuery(query.queryKey)) return false;
|
|
34
|
+
if (!staleOnly) return true;
|
|
35
|
+
const snapshot = query.queryKey.at(-1).session;
|
|
36
|
+
return !snapshot || snapshot.revision !== current.revision || snapshot.userId !== current.userId || snapshot.tenantId !== current.tenantId;
|
|
37
|
+
};
|
|
38
|
+
void cache.cancelQueries({ predicate }).catch(() => {
|
|
39
|
+
});
|
|
40
|
+
cache.removeQueries({ predicate });
|
|
41
|
+
for (const mutation of cache.getMutationCache().getAll()) {
|
|
42
|
+
const variables = mutation.state.variables;
|
|
43
|
+
const revision = variables && typeof variables === "object" && "revision" in variables ? variables.revision : void 0;
|
|
44
|
+
if (mutation.options.meta?.universalApiLayer === client.cacheScope && (!staleOnly || revision !== current.revision))
|
|
45
|
+
cache.getMutationCache().remove(mutation);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var cacheBindings = /* @__PURE__ */ new WeakMap();
|
|
49
|
+
function bindCache(client, cache) {
|
|
50
|
+
let caches = cacheBindings.get(client);
|
|
51
|
+
if (!caches) {
|
|
52
|
+
caches = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
cacheBindings.set(client, caches);
|
|
54
|
+
}
|
|
55
|
+
let binding = caches.get(cache);
|
|
56
|
+
if (!binding) {
|
|
57
|
+
clearClientCache(client, cache, true);
|
|
58
|
+
binding = {
|
|
59
|
+
count: 0,
|
|
60
|
+
unsubscribe: client.subscribeSession(
|
|
61
|
+
() => clearClientCache(client, cache)
|
|
62
|
+
)
|
|
63
|
+
};
|
|
64
|
+
caches.set(cache, binding);
|
|
65
|
+
}
|
|
66
|
+
binding.count++;
|
|
67
|
+
const current = binding;
|
|
68
|
+
return () => {
|
|
69
|
+
current.count--;
|
|
70
|
+
if (current.count === 0) {
|
|
71
|
+
current.unsubscribe();
|
|
72
|
+
clearClientCache(client, cache);
|
|
73
|
+
caches.delete(cache);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function ApiProvider({
|
|
78
|
+
client,
|
|
79
|
+
queryClient,
|
|
80
|
+
children
|
|
81
|
+
}) {
|
|
82
|
+
const [owned] = useState(() => queryClient ?? new QueryClient());
|
|
83
|
+
const cache = queryClient ?? owned;
|
|
84
|
+
const session = useSyncExternalStore(
|
|
85
|
+
client.subscribeSession,
|
|
86
|
+
client.getSessionSnapshot,
|
|
87
|
+
client.getSessionSnapshot
|
|
88
|
+
);
|
|
89
|
+
useEffect(() => bindCache(client, cache), [client, cache]);
|
|
90
|
+
const value = useMemo(() => ({ client, session }), [client, session]);
|
|
91
|
+
return /* @__PURE__ */ jsx(Context.Provider, { value, children: /* @__PURE__ */ jsx(QueryClientProvider, { client: cache, children }) });
|
|
92
|
+
}
|
|
93
|
+
function useContextValue() {
|
|
94
|
+
const value = useContext(Context);
|
|
95
|
+
if (!value) throw new Error("Wrap your application in ApiProvider");
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function useApiClient() {
|
|
99
|
+
return useContextValue().client;
|
|
100
|
+
}
|
|
101
|
+
function useApiSession() {
|
|
102
|
+
return useContextValue().session;
|
|
103
|
+
}
|
|
104
|
+
function useApiQuery(config) {
|
|
105
|
+
const { client, session } = useContextValue();
|
|
106
|
+
const cache = useQueryClient();
|
|
107
|
+
return useQuery({
|
|
108
|
+
queryKey: queryIdentity(
|
|
109
|
+
client,
|
|
110
|
+
config.key,
|
|
111
|
+
config.url,
|
|
112
|
+
config.params,
|
|
113
|
+
config.headers,
|
|
114
|
+
config.cacheScope
|
|
115
|
+
),
|
|
116
|
+
queryFn: ({ signal }) => {
|
|
117
|
+
client.assertSession(session.revision);
|
|
118
|
+
return client.request(config.url, {
|
|
119
|
+
params: config.params,
|
|
120
|
+
headers: config.headers,
|
|
121
|
+
timeoutMs: config.timeoutMs,
|
|
122
|
+
signal,
|
|
123
|
+
schema: config.schema,
|
|
124
|
+
authRetry: config.authRetry,
|
|
125
|
+
requiresAuth: config.requiresAuth
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
select: config.select ? (data) => invokeCallback(() => config.select(data)) : void 0,
|
|
129
|
+
enabled: config.requiresAuth ? !!session.userId && config.enabled !== false : config.enabled,
|
|
130
|
+
staleTime: config.staleTime,
|
|
131
|
+
retry: config.retry ?? cache.getDefaultOptions().queries?.retry ?? defaultQueryRetry
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function useApiMutation(config) {
|
|
135
|
+
const { client, session } = useContextValue();
|
|
136
|
+
const cache = useQueryClient();
|
|
137
|
+
const mutation = useMutation({
|
|
138
|
+
mutationKey: ["universal-api-layer", client.cacheScope, session.revision],
|
|
139
|
+
meta: { universalApiLayer: client.cacheScope },
|
|
140
|
+
mutationFn: async (envelope) => {
|
|
141
|
+
client.assertSession(envelope.revision);
|
|
142
|
+
const data = await runMutation(client, config, envelope.input);
|
|
143
|
+
client.assertSession(envelope.revision);
|
|
144
|
+
return data;
|
|
145
|
+
},
|
|
146
|
+
onSuccess: async (data, envelope) => {
|
|
147
|
+
client.assertSession(envelope.revision);
|
|
148
|
+
await Promise.all(
|
|
149
|
+
(config.invalidateKeys ?? []).map(
|
|
150
|
+
(queryKey) => cache.invalidateQueries({
|
|
151
|
+
queryKey,
|
|
152
|
+
predicate: config.invalidateScope === "all" ? void 0 : (query) => client.ownsQuery(query.queryKey)
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
client.assertSession(envelope.revision);
|
|
157
|
+
await invokeAsyncCallback(() => config.onSuccess?.(data, envelope.input));
|
|
158
|
+
client.assertSession(envelope.revision);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const translate = (options) => options ? {
|
|
162
|
+
onSuccess: (data, envelope, context, mutationContext) => {
|
|
163
|
+
if (envelope.revision === client.getSessionSnapshot().revision)
|
|
164
|
+
options.onSuccess?.(
|
|
165
|
+
data,
|
|
166
|
+
envelope.input,
|
|
167
|
+
context,
|
|
168
|
+
mutationContext
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
onError: (error, envelope, context, mutationContext) => {
|
|
172
|
+
if (envelope.revision === client.getSessionSnapshot().revision)
|
|
173
|
+
options.onError?.(
|
|
174
|
+
error,
|
|
175
|
+
envelope.input,
|
|
176
|
+
context,
|
|
177
|
+
mutationContext
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
onSettled: (data, error, envelope, context, mutationContext) => {
|
|
181
|
+
if (envelope.revision === client.getSessionSnapshot().revision)
|
|
182
|
+
options.onSettled?.(
|
|
183
|
+
data,
|
|
184
|
+
error,
|
|
185
|
+
envelope.input,
|
|
186
|
+
context,
|
|
187
|
+
mutationContext
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
} : void 0;
|
|
191
|
+
const current = !mutation.variables || mutation.variables.revision === session.revision;
|
|
192
|
+
return {
|
|
193
|
+
...mutation,
|
|
194
|
+
...!current ? {
|
|
195
|
+
data: void 0,
|
|
196
|
+
error: null,
|
|
197
|
+
context: void 0,
|
|
198
|
+
status: "idle",
|
|
199
|
+
isIdle: true,
|
|
200
|
+
isPending: false,
|
|
201
|
+
isSuccess: false,
|
|
202
|
+
isError: false,
|
|
203
|
+
isPaused: false,
|
|
204
|
+
submittedAt: 0,
|
|
205
|
+
failureCount: 0,
|
|
206
|
+
failureReason: null
|
|
207
|
+
} : {},
|
|
208
|
+
variables: current ? mutation.variables?.input : void 0,
|
|
209
|
+
mutate: (input, options) => mutation.mutate(
|
|
210
|
+
{ input, revision: client.getSessionSnapshot().revision },
|
|
211
|
+
translate(options)
|
|
212
|
+
),
|
|
213
|
+
mutateAsync: (input, options) => client.withSession(
|
|
214
|
+
() => mutation.mutateAsync(
|
|
215
|
+
{ input, revision: client.getSessionSnapshot().revision },
|
|
216
|
+
translate(options)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export {
|
|
222
|
+
ApiProvider,
|
|
223
|
+
useApiClient,
|
|
224
|
+
useApiMutation,
|
|
225
|
+
useApiQuery,
|
|
226
|
+
useApiSession
|
|
227
|
+
};
|