khotan-data 0.0.1 → 0.1.1
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/AGENTS.md +54 -0
- package/README.md +117 -1
- package/dist/cli.js +2869 -0
- package/dist/factory.cjs +3303 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +662 -0
- package/dist/factory.d.ts +662 -0
- package/dist/factory.js +3292 -0
- package/dist/factory.js.map +1 -0
- package/dist/plug-client.cjs +99 -0
- package/dist/plug-client.cjs.map +1 -0
- package/dist/plug-client.d.cts +71 -0
- package/dist/plug-client.d.ts +71 -0
- package/dist/plug-client.js +96 -0
- package/dist/plug-client.js.map +1 -0
- package/dist/templates/agent-skill.md +73 -0
- package/dist/templates/agents.md +41 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.example.ts +36 -0
- package/dist/templates/catch.ts +119 -0
- package/dist/templates/config-page.tsx +20 -0
- package/dist/templates/debug-index-page.tsx +101 -0
- package/dist/templates/debug-page.tsx +48 -0
- package/dist/templates/graph-page.tsx +11 -0
- package/dist/templates/hub.tsx +450 -0
- package/dist/templates/inflow.example.ts +61 -0
- package/dist/templates/inflow.ts +98 -0
- package/dist/templates/khotan-config.ts +49 -0
- package/dist/templates/khotan-route.ts +13 -0
- package/dist/templates/logs-page.tsx +9 -0
- package/dist/templates/logs.tsx +20 -0
- package/dist/templates/mapping-browser.tsx +761 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.example.ts +52 -0
- package/dist/templates/outflow.ts +90 -0
- package/dist/templates/pass.example.ts +51 -0
- package/dist/templates/pass.ts +134 -0
- package/dist/templates/plug-debugger.tsx +1185 -0
- package/dist/templates/plug.example.ts +93 -0
- package/dist/templates/plug.ts +806 -0
- package/dist/templates/relay.example.ts +71 -0
- package/dist/templates/relay.ts +104 -0
- package/dist/templates/runs-table.tsx +592 -0
- package/dist/templates/schema.ts +505 -0
- package/dist/templates/skill-dashboard.md +144 -0
- package/dist/templates/skill-plug.md +216 -0
- package/dist/templates/skill-setup.md +161 -0
- package/dist/templates/skill-webhook.md +196 -0
- package/dist/templates/topology-canvas.tsx +1406 -0
- package/dist/templates/var-panel.tsx +276 -0
- package/dist/templates/webhook-events-table.tsx +241 -0
- package/dist/templates/wire-panel.tsx +216 -0
- package/dist/templates/wire.ts +155 -0
- package/package.json +46 -5
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/plug-client.ts
|
|
4
|
+
function defineContract(contract) {
|
|
5
|
+
return contract;
|
|
6
|
+
}
|
|
7
|
+
function isPlugError(err) {
|
|
8
|
+
return err instanceof Error && "status" in err && typeof err.status === "number" && "body" in err;
|
|
9
|
+
}
|
|
10
|
+
function parseBody(body) {
|
|
11
|
+
if (typeof body === "string") {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(body);
|
|
14
|
+
} catch {
|
|
15
|
+
return body;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
20
|
+
function interpolatePath(path, params) {
|
|
21
|
+
return path.replace(/:([^/]+)/g, (_, key) => {
|
|
22
|
+
const value = params[key];
|
|
23
|
+
if (value === void 0) {
|
|
24
|
+
throw new Error(`Missing path parameter: "${key}"`);
|
|
25
|
+
}
|
|
26
|
+
return encodeURIComponent(value);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createPlugClient(contract, plug, options) {
|
|
30
|
+
const globalValidateResponse = options?.validateResponse ?? true;
|
|
31
|
+
function buildClient(router) {
|
|
32
|
+
const client = {};
|
|
33
|
+
for (const [key, value] of Object.entries(router)) {
|
|
34
|
+
const route = value;
|
|
35
|
+
if ("method" in route && "path" in route) {
|
|
36
|
+
client[key] = async (input) => {
|
|
37
|
+
const def = route;
|
|
38
|
+
const params = input?.["params"] ?? {};
|
|
39
|
+
const query = input?.["query"];
|
|
40
|
+
const body = input?.["body"];
|
|
41
|
+
const headers = input?.["headers"];
|
|
42
|
+
const shouldValidateResponse = input?.["validateResponse"] ?? globalValidateResponse;
|
|
43
|
+
if (def.pathParams) {
|
|
44
|
+
def.pathParams.parse(params);
|
|
45
|
+
}
|
|
46
|
+
if (def.query && query !== void 0) {
|
|
47
|
+
def.query.parse(query);
|
|
48
|
+
}
|
|
49
|
+
if (def.body && body !== void 0) {
|
|
50
|
+
def.body.parse(body);
|
|
51
|
+
}
|
|
52
|
+
const interpolatedPath = interpolatePath(def.path, params);
|
|
53
|
+
const responses = def.responses;
|
|
54
|
+
try {
|
|
55
|
+
const responseBody = await plug.request(
|
|
56
|
+
def.method,
|
|
57
|
+
interpolatedPath,
|
|
58
|
+
{
|
|
59
|
+
...query !== void 0 ? { params: query } : {},
|
|
60
|
+
...body !== void 0 ? { body } : {},
|
|
61
|
+
...headers !== void 0 ? { headers } : {}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
const status = 200;
|
|
65
|
+
if (!responses[status]) {
|
|
66
|
+
return { status, body: responseBody };
|
|
67
|
+
}
|
|
68
|
+
if (shouldValidateResponse) {
|
|
69
|
+
const validated = responses[status].parse(responseBody);
|
|
70
|
+
return { status, body: validated };
|
|
71
|
+
}
|
|
72
|
+
return { status, body: responseBody };
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (!isPlugError(err)) throw err;
|
|
75
|
+
const status = err.status;
|
|
76
|
+
if (responses[status]) {
|
|
77
|
+
const parsed = parseBody(err.body);
|
|
78
|
+
if (shouldValidateResponse) {
|
|
79
|
+
const validated = responses[status].parse(parsed);
|
|
80
|
+
return { status, body: validated };
|
|
81
|
+
}
|
|
82
|
+
return { status, body: parsed };
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
} else {
|
|
88
|
+
client[key] = buildClient(route);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return client;
|
|
92
|
+
}
|
|
93
|
+
return buildClient(contract);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
exports.createPlugClient = createPlugClient;
|
|
97
|
+
exports.defineContract = defineContract;
|
|
98
|
+
//# sourceMappingURL=plug-client.cjs.map
|
|
99
|
+
//# sourceMappingURL=plug-client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plug-client.ts"],"names":[],"mappings":"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","file":"plug-client.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Schema interface — works with zod v3, v4, or any validator with .parse()\n// ---------------------------------------------------------------------------\n\nexport interface Schema<TOutput = unknown, TInput = unknown> {\n parse(data: unknown): TOutput;\n _input?: TInput;\n _output?: TOutput;\n}\n\n// ---------------------------------------------------------------------------\n// Contract types\n// ---------------------------------------------------------------------------\n\nexport interface RouteDefinition {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Schema;\n body?: Schema;\n pathParams?: Schema;\n responses: Record<number, Schema>;\n}\n\nexport interface ContractRouter {\n [key: string]: RouteDefinition | ContractRouter;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level utilities\n// ---------------------------------------------------------------------------\n\ntype SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;\ntype SchemaOutput<T> = T extends Schema<infer O> ? O : never;\n\ntype PathParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Record<Param | keyof PathParams<Rest>, string>\n : T extends `${string}:${infer Param}`\n ? Record<Param, string>\n : never;\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype EndpointInput<TRoute extends RouteDefinition> = (HasKeys<\n PathParams<TRoute[\"path\"]>\n> extends true\n ? { params: PathParams<TRoute[\"path\"]> }\n : { params?: never }) &\n (TRoute extends { query: Schema }\n ? { query: SchemaInput<TRoute[\"query\"]> }\n : { query?: never }) &\n (TRoute extends { body: Schema }\n ? { body: SchemaInput<TRoute[\"body\"]> }\n : { body?: never }) & {\n headers?: Record<string, string>;\n validateResponse?: boolean;\n };\n\ntype ResponseSchemas<TRoute extends RouteDefinition> = {\n [K in keyof TRoute[\"responses\"]]: K extends number\n ? { status: K; body: SchemaOutput<TRoute[\"responses\"][K]> }\n : never;\n}[keyof TRoute[\"responses\"]];\n\ntype PlugClient<T extends ContractRouter> = {\n [K in keyof T]: T[K] extends RouteDefinition\n ? HasKeys<PathParams<T[K][\"path\"]>> extends true\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { query: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { body: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends ContractRouter\n ? PlugClient<T[K]>\n : never;\n};\n\n// ---------------------------------------------------------------------------\n// defineContract — type-narrowing identity function\n// ---------------------------------------------------------------------------\n\nexport function defineContract<const T extends ContractRouter>(contract: T): T {\n return contract;\n}\n\n// ---------------------------------------------------------------------------\n// PlugLike interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface expected from a Plug instance.\n * Matches the scaffolded Plug class's public API.\n */\nexport interface PlugLike {\n request<T>(\n method: string,\n path: string,\n options?: {\n params?: Record<string, unknown>;\n body?: unknown;\n headers?: Record<string, string>;\n },\n ): Promise<T>;\n}\n\nexport interface PlugClientOptions {\n validateResponse?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// PlugError detection — works without importing the user's PlugError class\n// ---------------------------------------------------------------------------\n\ninterface PlugErrorLike {\n name: string;\n status: number;\n body: unknown;\n}\n\nfunction isPlugError(err: unknown): err is PlugErrorLike {\n return (\n err instanceof Error &&\n \"status\" in err &&\n typeof (err as unknown as PlugErrorLike).status === \"number\" &&\n \"body\" in err\n );\n}\n\nfunction parseBody(body: unknown): unknown {\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body);\n } catch {\n return body;\n }\n }\n return body;\n}\n\n// ---------------------------------------------------------------------------\n// Path interpolation\n// ---------------------------------------------------------------------------\n\nfunction interpolatePath(path: string, params: Record<string, string>): string {\n return path.replace(/:([^/]+)/g, (_, key: string) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: \"${key}\"`);\n }\n return encodeURIComponent(value);\n });\n}\n\n// ---------------------------------------------------------------------------\n// createPlugClient\n// ---------------------------------------------------------------------------\n\nexport function createPlugClient<T extends ContractRouter>(\n contract: T,\n plug: PlugLike,\n options?: PlugClientOptions,\n): PlugClient<T> {\n const globalValidateResponse = options?.validateResponse ?? true;\n\n function buildClient(router: ContractRouter): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(router)) {\n const route = value;\n\n if (\"method\" in route && \"path\" in route) {\n client[key] = async (input?: Record<string, unknown>) => {\n const def = route as RouteDefinition;\n const params = (input?.[\"params\"] ?? {}) as Record<string, string>;\n const query = input?.[\"query\"] as Record<string, unknown> | undefined;\n const body = input?.[\"body\"];\n const headers = input?.[\"headers\"] as\n | Record<string, string>\n | undefined;\n const shouldValidateResponse =\n (input?.[\"validateResponse\"] as boolean | undefined) ??\n globalValidateResponse;\n\n if (def.pathParams) {\n def.pathParams.parse(params);\n }\n if (def.query && query !== undefined) {\n def.query.parse(query);\n }\n if (def.body && body !== undefined) {\n def.body.parse(body);\n }\n\n const interpolatedPath = interpolatePath(def.path, params);\n const responses = def.responses;\n\n try {\n const responseBody = await plug.request<unknown>(\n def.method,\n interpolatedPath,\n {\n ...(query !== undefined ? { params: query } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(headers !== undefined ? { headers } : {}),\n },\n );\n\n const status = 200;\n if (!responses[status]) {\n return { status, body: responseBody };\n }\n\n if (shouldValidateResponse) {\n const validated = responses[status].parse(responseBody);\n return { status, body: validated };\n }\n\n return { status, body: responseBody };\n } catch (err) {\n if (!isPlugError(err)) throw err;\n\n const status = err.status;\n if (responses[status]) {\n const parsed = parseBody(err.body);\n if (shouldValidateResponse) {\n const validated = responses[status].parse(parsed);\n return { status, body: validated };\n }\n return { status, body: parsed };\n }\n\n throw err;\n }\n };\n } else {\n client[key] = buildClient(route);\n }\n }\n\n return client;\n }\n\n return buildClient(contract) as PlugClient<T>;\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
interface Schema<TOutput = unknown, TInput = unknown> {
|
|
2
|
+
parse(data: unknown): TOutput;
|
|
3
|
+
_input?: TInput;
|
|
4
|
+
_output?: TOutput;
|
|
5
|
+
}
|
|
6
|
+
interface RouteDefinition {
|
|
7
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
8
|
+
path: string;
|
|
9
|
+
query?: Schema;
|
|
10
|
+
body?: Schema;
|
|
11
|
+
pathParams?: Schema;
|
|
12
|
+
responses: Record<number, Schema>;
|
|
13
|
+
}
|
|
14
|
+
interface ContractRouter {
|
|
15
|
+
[key: string]: RouteDefinition | ContractRouter;
|
|
16
|
+
}
|
|
17
|
+
type SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;
|
|
18
|
+
type SchemaOutput<T> = T extends Schema<infer O> ? O : never;
|
|
19
|
+
type PathParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Record<Param | keyof PathParams<Rest>, string> : T extends `${string}:${infer Param}` ? Record<Param, string> : never;
|
|
20
|
+
type HasKeys<T> = keyof T extends never ? false : true;
|
|
21
|
+
type EndpointInput<TRoute extends RouteDefinition> = (HasKeys<PathParams<TRoute["path"]>> extends true ? {
|
|
22
|
+
params: PathParams<TRoute["path"]>;
|
|
23
|
+
} : {
|
|
24
|
+
params?: never;
|
|
25
|
+
}) & (TRoute extends {
|
|
26
|
+
query: Schema;
|
|
27
|
+
} ? {
|
|
28
|
+
query: SchemaInput<TRoute["query"]>;
|
|
29
|
+
} : {
|
|
30
|
+
query?: never;
|
|
31
|
+
}) & (TRoute extends {
|
|
32
|
+
body: Schema;
|
|
33
|
+
} ? {
|
|
34
|
+
body: SchemaInput<TRoute["body"]>;
|
|
35
|
+
} : {
|
|
36
|
+
body?: never;
|
|
37
|
+
}) & {
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
validateResponse?: boolean;
|
|
40
|
+
};
|
|
41
|
+
type ResponseSchemas<TRoute extends RouteDefinition> = {
|
|
42
|
+
[K in keyof TRoute["responses"]]: K extends number ? {
|
|
43
|
+
status: K;
|
|
44
|
+
body: SchemaOutput<TRoute["responses"][K]>;
|
|
45
|
+
} : never;
|
|
46
|
+
}[keyof TRoute["responses"]];
|
|
47
|
+
type PlugClient<T extends ContractRouter> = {
|
|
48
|
+
[K in keyof T]: T[K] extends RouteDefinition ? HasKeys<PathParams<T[K]["path"]>> extends true ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
49
|
+
query: Schema;
|
|
50
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
51
|
+
body: Schema;
|
|
52
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends ContractRouter ? PlugClient<T[K]> : never;
|
|
53
|
+
};
|
|
54
|
+
declare function defineContract<const T extends ContractRouter>(contract: T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Minimal interface expected from a Plug instance.
|
|
57
|
+
* Matches the scaffolded Plug class's public API.
|
|
58
|
+
*/
|
|
59
|
+
interface PlugLike {
|
|
60
|
+
request<T>(method: string, path: string, options?: {
|
|
61
|
+
params?: Record<string, unknown>;
|
|
62
|
+
body?: unknown;
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
}): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
interface PlugClientOptions {
|
|
67
|
+
validateResponse?: boolean;
|
|
68
|
+
}
|
|
69
|
+
declare function createPlugClient<T extends ContractRouter>(contract: T, plug: PlugLike, options?: PlugClientOptions): PlugClient<T>;
|
|
70
|
+
|
|
71
|
+
export { type ContractRouter, type PlugClientOptions, type PlugLike, type RouteDefinition, type Schema, createPlugClient, defineContract };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
interface Schema<TOutput = unknown, TInput = unknown> {
|
|
2
|
+
parse(data: unknown): TOutput;
|
|
3
|
+
_input?: TInput;
|
|
4
|
+
_output?: TOutput;
|
|
5
|
+
}
|
|
6
|
+
interface RouteDefinition {
|
|
7
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
8
|
+
path: string;
|
|
9
|
+
query?: Schema;
|
|
10
|
+
body?: Schema;
|
|
11
|
+
pathParams?: Schema;
|
|
12
|
+
responses: Record<number, Schema>;
|
|
13
|
+
}
|
|
14
|
+
interface ContractRouter {
|
|
15
|
+
[key: string]: RouteDefinition | ContractRouter;
|
|
16
|
+
}
|
|
17
|
+
type SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;
|
|
18
|
+
type SchemaOutput<T> = T extends Schema<infer O> ? O : never;
|
|
19
|
+
type PathParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Record<Param | keyof PathParams<Rest>, string> : T extends `${string}:${infer Param}` ? Record<Param, string> : never;
|
|
20
|
+
type HasKeys<T> = keyof T extends never ? false : true;
|
|
21
|
+
type EndpointInput<TRoute extends RouteDefinition> = (HasKeys<PathParams<TRoute["path"]>> extends true ? {
|
|
22
|
+
params: PathParams<TRoute["path"]>;
|
|
23
|
+
} : {
|
|
24
|
+
params?: never;
|
|
25
|
+
}) & (TRoute extends {
|
|
26
|
+
query: Schema;
|
|
27
|
+
} ? {
|
|
28
|
+
query: SchemaInput<TRoute["query"]>;
|
|
29
|
+
} : {
|
|
30
|
+
query?: never;
|
|
31
|
+
}) & (TRoute extends {
|
|
32
|
+
body: Schema;
|
|
33
|
+
} ? {
|
|
34
|
+
body: SchemaInput<TRoute["body"]>;
|
|
35
|
+
} : {
|
|
36
|
+
body?: never;
|
|
37
|
+
}) & {
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
validateResponse?: boolean;
|
|
40
|
+
};
|
|
41
|
+
type ResponseSchemas<TRoute extends RouteDefinition> = {
|
|
42
|
+
[K in keyof TRoute["responses"]]: K extends number ? {
|
|
43
|
+
status: K;
|
|
44
|
+
body: SchemaOutput<TRoute["responses"][K]>;
|
|
45
|
+
} : never;
|
|
46
|
+
}[keyof TRoute["responses"]];
|
|
47
|
+
type PlugClient<T extends ContractRouter> = {
|
|
48
|
+
[K in keyof T]: T[K] extends RouteDefinition ? HasKeys<PathParams<T[K]["path"]>> extends true ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
49
|
+
query: Schema;
|
|
50
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
|
|
51
|
+
body: Schema;
|
|
52
|
+
} ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends ContractRouter ? PlugClient<T[K]> : never;
|
|
53
|
+
};
|
|
54
|
+
declare function defineContract<const T extends ContractRouter>(contract: T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Minimal interface expected from a Plug instance.
|
|
57
|
+
* Matches the scaffolded Plug class's public API.
|
|
58
|
+
*/
|
|
59
|
+
interface PlugLike {
|
|
60
|
+
request<T>(method: string, path: string, options?: {
|
|
61
|
+
params?: Record<string, unknown>;
|
|
62
|
+
body?: unknown;
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
}): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
interface PlugClientOptions {
|
|
67
|
+
validateResponse?: boolean;
|
|
68
|
+
}
|
|
69
|
+
declare function createPlugClient<T extends ContractRouter>(contract: T, plug: PlugLike, options?: PlugClientOptions): PlugClient<T>;
|
|
70
|
+
|
|
71
|
+
export { type ContractRouter, type PlugClientOptions, type PlugLike, type RouteDefinition, type Schema, createPlugClient, defineContract };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/plug-client.ts
|
|
2
|
+
function defineContract(contract) {
|
|
3
|
+
return contract;
|
|
4
|
+
}
|
|
5
|
+
function isPlugError(err) {
|
|
6
|
+
return err instanceof Error && "status" in err && typeof err.status === "number" && "body" in err;
|
|
7
|
+
}
|
|
8
|
+
function parseBody(body) {
|
|
9
|
+
if (typeof body === "string") {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(body);
|
|
12
|
+
} catch {
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
function interpolatePath(path, params) {
|
|
19
|
+
return path.replace(/:([^/]+)/g, (_, key) => {
|
|
20
|
+
const value = params[key];
|
|
21
|
+
if (value === void 0) {
|
|
22
|
+
throw new Error(`Missing path parameter: "${key}"`);
|
|
23
|
+
}
|
|
24
|
+
return encodeURIComponent(value);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function createPlugClient(contract, plug, options) {
|
|
28
|
+
const globalValidateResponse = options?.validateResponse ?? true;
|
|
29
|
+
function buildClient(router) {
|
|
30
|
+
const client = {};
|
|
31
|
+
for (const [key, value] of Object.entries(router)) {
|
|
32
|
+
const route = value;
|
|
33
|
+
if ("method" in route && "path" in route) {
|
|
34
|
+
client[key] = async (input) => {
|
|
35
|
+
const def = route;
|
|
36
|
+
const params = input?.["params"] ?? {};
|
|
37
|
+
const query = input?.["query"];
|
|
38
|
+
const body = input?.["body"];
|
|
39
|
+
const headers = input?.["headers"];
|
|
40
|
+
const shouldValidateResponse = input?.["validateResponse"] ?? globalValidateResponse;
|
|
41
|
+
if (def.pathParams) {
|
|
42
|
+
def.pathParams.parse(params);
|
|
43
|
+
}
|
|
44
|
+
if (def.query && query !== void 0) {
|
|
45
|
+
def.query.parse(query);
|
|
46
|
+
}
|
|
47
|
+
if (def.body && body !== void 0) {
|
|
48
|
+
def.body.parse(body);
|
|
49
|
+
}
|
|
50
|
+
const interpolatedPath = interpolatePath(def.path, params);
|
|
51
|
+
const responses = def.responses;
|
|
52
|
+
try {
|
|
53
|
+
const responseBody = await plug.request(
|
|
54
|
+
def.method,
|
|
55
|
+
interpolatedPath,
|
|
56
|
+
{
|
|
57
|
+
...query !== void 0 ? { params: query } : {},
|
|
58
|
+
...body !== void 0 ? { body } : {},
|
|
59
|
+
...headers !== void 0 ? { headers } : {}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
const status = 200;
|
|
63
|
+
if (!responses[status]) {
|
|
64
|
+
return { status, body: responseBody };
|
|
65
|
+
}
|
|
66
|
+
if (shouldValidateResponse) {
|
|
67
|
+
const validated = responses[status].parse(responseBody);
|
|
68
|
+
return { status, body: validated };
|
|
69
|
+
}
|
|
70
|
+
return { status, body: responseBody };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (!isPlugError(err)) throw err;
|
|
73
|
+
const status = err.status;
|
|
74
|
+
if (responses[status]) {
|
|
75
|
+
const parsed = parseBody(err.body);
|
|
76
|
+
if (shouldValidateResponse) {
|
|
77
|
+
const validated = responses[status].parse(parsed);
|
|
78
|
+
return { status, body: validated };
|
|
79
|
+
}
|
|
80
|
+
return { status, body: parsed };
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
client[key] = buildClient(route);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return client;
|
|
90
|
+
}
|
|
91
|
+
return buildClient(contract);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { createPlugClient, defineContract };
|
|
95
|
+
//# sourceMappingURL=plug-client.js.map
|
|
96
|
+
//# sourceMappingURL=plug-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plug-client.ts"],"names":[],"mappings":"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","file":"plug-client.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Schema interface — works with zod v3, v4, or any validator with .parse()\n// ---------------------------------------------------------------------------\n\nexport interface Schema<TOutput = unknown, TInput = unknown> {\n parse(data: unknown): TOutput;\n _input?: TInput;\n _output?: TOutput;\n}\n\n// ---------------------------------------------------------------------------\n// Contract types\n// ---------------------------------------------------------------------------\n\nexport interface RouteDefinition {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Schema;\n body?: Schema;\n pathParams?: Schema;\n responses: Record<number, Schema>;\n}\n\nexport interface ContractRouter {\n [key: string]: RouteDefinition | ContractRouter;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level utilities\n// ---------------------------------------------------------------------------\n\ntype SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;\ntype SchemaOutput<T> = T extends Schema<infer O> ? O : never;\n\ntype PathParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Record<Param | keyof PathParams<Rest>, string>\n : T extends `${string}:${infer Param}`\n ? Record<Param, string>\n : never;\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype EndpointInput<TRoute extends RouteDefinition> = (HasKeys<\n PathParams<TRoute[\"path\"]>\n> extends true\n ? { params: PathParams<TRoute[\"path\"]> }\n : { params?: never }) &\n (TRoute extends { query: Schema }\n ? { query: SchemaInput<TRoute[\"query\"]> }\n : { query?: never }) &\n (TRoute extends { body: Schema }\n ? { body: SchemaInput<TRoute[\"body\"]> }\n : { body?: never }) & {\n headers?: Record<string, string>;\n validateResponse?: boolean;\n };\n\ntype ResponseSchemas<TRoute extends RouteDefinition> = {\n [K in keyof TRoute[\"responses\"]]: K extends number\n ? { status: K; body: SchemaOutput<TRoute[\"responses\"][K]> }\n : never;\n}[keyof TRoute[\"responses\"]];\n\ntype PlugClient<T extends ContractRouter> = {\n [K in keyof T]: T[K] extends RouteDefinition\n ? HasKeys<PathParams<T[K][\"path\"]>> extends true\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { query: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { body: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends ContractRouter\n ? PlugClient<T[K]>\n : never;\n};\n\n// ---------------------------------------------------------------------------\n// defineContract — type-narrowing identity function\n// ---------------------------------------------------------------------------\n\nexport function defineContract<const T extends ContractRouter>(contract: T): T {\n return contract;\n}\n\n// ---------------------------------------------------------------------------\n// PlugLike interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface expected from a Plug instance.\n * Matches the scaffolded Plug class's public API.\n */\nexport interface PlugLike {\n request<T>(\n method: string,\n path: string,\n options?: {\n params?: Record<string, unknown>;\n body?: unknown;\n headers?: Record<string, string>;\n },\n ): Promise<T>;\n}\n\nexport interface PlugClientOptions {\n validateResponse?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// PlugError detection — works without importing the user's PlugError class\n// ---------------------------------------------------------------------------\n\ninterface PlugErrorLike {\n name: string;\n status: number;\n body: unknown;\n}\n\nfunction isPlugError(err: unknown): err is PlugErrorLike {\n return (\n err instanceof Error &&\n \"status\" in err &&\n typeof (err as unknown as PlugErrorLike).status === \"number\" &&\n \"body\" in err\n );\n}\n\nfunction parseBody(body: unknown): unknown {\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body);\n } catch {\n return body;\n }\n }\n return body;\n}\n\n// ---------------------------------------------------------------------------\n// Path interpolation\n// ---------------------------------------------------------------------------\n\nfunction interpolatePath(path: string, params: Record<string, string>): string {\n return path.replace(/:([^/]+)/g, (_, key: string) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: \"${key}\"`);\n }\n return encodeURIComponent(value);\n });\n}\n\n// ---------------------------------------------------------------------------\n// createPlugClient\n// ---------------------------------------------------------------------------\n\nexport function createPlugClient<T extends ContractRouter>(\n contract: T,\n plug: PlugLike,\n options?: PlugClientOptions,\n): PlugClient<T> {\n const globalValidateResponse = options?.validateResponse ?? true;\n\n function buildClient(router: ContractRouter): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(router)) {\n const route = value;\n\n if (\"method\" in route && \"path\" in route) {\n client[key] = async (input?: Record<string, unknown>) => {\n const def = route as RouteDefinition;\n const params = (input?.[\"params\"] ?? {}) as Record<string, string>;\n const query = input?.[\"query\"] as Record<string, unknown> | undefined;\n const body = input?.[\"body\"];\n const headers = input?.[\"headers\"] as\n | Record<string, string>\n | undefined;\n const shouldValidateResponse =\n (input?.[\"validateResponse\"] as boolean | undefined) ??\n globalValidateResponse;\n\n if (def.pathParams) {\n def.pathParams.parse(params);\n }\n if (def.query && query !== undefined) {\n def.query.parse(query);\n }\n if (def.body && body !== undefined) {\n def.body.parse(body);\n }\n\n const interpolatedPath = interpolatePath(def.path, params);\n const responses = def.responses;\n\n try {\n const responseBody = await plug.request<unknown>(\n def.method,\n interpolatedPath,\n {\n ...(query !== undefined ? { params: query } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(headers !== undefined ? { headers } : {}),\n },\n );\n\n const status = 200;\n if (!responses[status]) {\n return { status, body: responseBody };\n }\n\n if (shouldValidateResponse) {\n const validated = responses[status].parse(responseBody);\n return { status, body: validated };\n }\n\n return { status, body: responseBody };\n } catch (err) {\n if (!isPlugError(err)) throw err;\n\n const status = err.status;\n if (responses[status]) {\n const parsed = parseBody(err.body);\n if (shouldValidateResponse) {\n const validated = responses[status].parse(parsed);\n return { status, body: validated };\n }\n return { status, body: parsed };\n }\n\n throw err;\n }\n };\n } else {\n client[key] = buildClient(route);\n }\n }\n\n return client;\n }\n\n return buildClient(contract) as PlugClient<T>;\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: khotan-probe
|
|
3
|
+
description: >
|
|
4
|
+
Inspect and debug khotan plugs via the CLI. Prefer `khotan plug`
|
|
5
|
+
(legacy alias: `khotan probe`). Use when verifying API
|
|
6
|
+
response shapes against typed endpoint definitions, debugging type
|
|
7
|
+
mismatches between declared schemas and actual responses, or exploring
|
|
8
|
+
available plugs and their endpoints.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Inspect and debug khotan plugs via the CLI. Prefer `khotan plug` (legacy alias: `khotan probe`). Use when verifying API response shapes against typed endpoint definitions, debugging type mismatches between declared schemas and actual responses, or exploring available plugs and their endpoints.
|
|
12
|
+
|
|
13
|
+
**Requires**: A running dev server with `KHOTAN_DEBUG=1` set.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### List all plugs
|
|
18
|
+
```bash
|
|
19
|
+
npx khotan plug --list
|
|
20
|
+
```
|
|
21
|
+
Returns: `{ ok, plugs: [{ name, baseUrl, authType, varsConfigured }] }`
|
|
22
|
+
|
|
23
|
+
### Show plug info and endpoints
|
|
24
|
+
```bash
|
|
25
|
+
npx khotan plug <plugName> --info
|
|
26
|
+
```
|
|
27
|
+
Returns: `{ ok, plug: { name, baseUrl, authType, vars, endpoints } }`
|
|
28
|
+
|
|
29
|
+
### Fire a request through a plug
|
|
30
|
+
```bash
|
|
31
|
+
npx khotan plug <plugName> GET /products
|
|
32
|
+
npx khotan plug <plugName> POST /subscriptions --body '{"url":"https://example.com"}'
|
|
33
|
+
npx khotan plug <plugName> GET /products --params '{"limit":"10"}'
|
|
34
|
+
npx khotan plug <plugName> GET /products --headers '{"X-Custom":"value"}'
|
|
35
|
+
```
|
|
36
|
+
Returns: `{ ok, request, response: { status, timing, size, body }, matchedEndpoint }`
|
|
37
|
+
|
|
38
|
+
### Fire via named endpoint
|
|
39
|
+
```bash
|
|
40
|
+
npx khotan plug <plugName> --endpoint listProducts
|
|
41
|
+
```
|
|
42
|
+
Resolves method and path from the endpoint definition automatically.
|
|
43
|
+
|
|
44
|
+
### Compare response against schema
|
|
45
|
+
```bash
|
|
46
|
+
npx khotan plug <plugName> --endpoint listProducts --compare
|
|
47
|
+
```
|
|
48
|
+
Returns: `{ ..., comparison: { match, expected, actual, mismatches } }`
|
|
49
|
+
|
|
50
|
+
Each mismatch has `{ path, issue, note }` where issue is `missing`, `extra`, or `type_mismatch` and path uses JSONPath notation (e.g. `$.items[].sku`).
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
| Flag | Description |
|
|
55
|
+
|------|-------------|
|
|
56
|
+
| `--port <n>` | Dev server port (default: from .env.local → .env → 3000) |
|
|
57
|
+
| `--base-path <p>` | API base path (default: `/api/khotan`) |
|
|
58
|
+
| `--list` | List registered plugs |
|
|
59
|
+
| `--info` | Show plug metadata |
|
|
60
|
+
| `--endpoint <name>` | Fire using named endpoint |
|
|
61
|
+
| `--compare` | Diff response against schema |
|
|
62
|
+
| `--body <json>` | Request body |
|
|
63
|
+
| `--params <json>` | Query params |
|
|
64
|
+
| `--headers <json>` | Extra headers |
|
|
65
|
+
|
|
66
|
+
## Workflow
|
|
67
|
+
|
|
68
|
+
1. **Discover**: `--list` to find plugs, `--info` to see endpoints
|
|
69
|
+
2. **Probe**: Fire a request via endpoint name or raw method/path
|
|
70
|
+
3. **Compare**: Add `--compare` to check response shape against Zod schema
|
|
71
|
+
4. **Fix**: If mismatches found, update the endpoint's response schema or adjust API expectations
|
|
72
|
+
|
|
73
|
+
All output is JSON on stdout — parse with standard JSON tools.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# khotan-data
|
|
2
|
+
|
|
3
|
+
Production-grade data flow, ETL, and webhook components for Next.js + Drizzle + Postgres.
|
|
4
|
+
|
|
5
|
+
## Skills
|
|
6
|
+
|
|
7
|
+
| Skill | When to use |
|
|
8
|
+
|-------|-------------|
|
|
9
|
+
| [khotan-setup](skills/khotan-setup/SKILL.md) | Initializing khotan in a new project, adding the database schema, configuring the factory |
|
|
10
|
+
| [khotan-plug](skills/khotan-plug/SKILL.md) | Connecting to a new API, defining endpoint contracts, configuring authentication |
|
|
11
|
+
| [khotan-dashboard](skills/khotan-dashboard/SKILL.md) | Adding a management interface, configuring plug variables in the browser |
|
|
12
|
+
| [khotan-webhook](skills/khotan-webhook/SKILL.md) | Receiving webhooks, registering callback URLs, processing incoming events |
|
|
13
|
+
| [khotan-probe](skills/khotan-probe/SKILL.md) | Debugging plugs via CLI with `khotan plug` (legacy alias: `probe`) |
|
|
14
|
+
|
|
15
|
+
## Quick Reference
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx khotan init # Initialize project
|
|
19
|
+
npx khotan add plug --yes # Add HTTP client component
|
|
20
|
+
npx khotan add hub --yes # Add dashboard UI
|
|
21
|
+
npx khotan generate # Scaffold Drizzle schema
|
|
22
|
+
npx khotan migrate # Apply database migrations
|
|
23
|
+
npx khotan plug --list # Debug: list registered plugs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Key Files
|
|
27
|
+
|
|
28
|
+
| File | Purpose |
|
|
29
|
+
|------|---------|
|
|
30
|
+
| `khotan.config.ts` | CLI config — sets outputDir |
|
|
31
|
+
| `{outputDir}/khotan.ts` | Factory config — register plugs, resources, adapter |
|
|
32
|
+
| `src/app/api/khotan/[...all]/route.ts` | Catch-all API route |
|
|
33
|
+
|
|
34
|
+
## Environment Variables
|
|
35
|
+
|
|
36
|
+
| Variable | Purpose |
|
|
37
|
+
|----------|---------|
|
|
38
|
+
| `DATABASE_URL` | Postgres connection (Drizzle) |
|
|
39
|
+
| `KHOTAN_SECRET` | AES-256-GCM key for encrypting plug variables |
|
|
40
|
+
| `KHOTAN_DEBUG` | Enables debug routes and the `plug` CLI (`probe` alias) |
|
|
41
|
+
| `KHOTAN_WEBHOOK_URL` | Public URL for webhook callbacks |
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Cache — durable sync-state storage for flows, relays, and webhooks
|
|
3
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
4
|
+
//
|
|
5
|
+
// This file defines the cache() builder and types. Create named cache
|
|
6
|
+
// definitions for expensive upstream snapshots, checkpoints, and dedupe markers,
|
|
7
|
+
// then register them in your khotan.ts factory config.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
import type { CacheRegistration, CacheScope } from "khotan-data/factory";
|
|
11
|
+
|
|
12
|
+
export interface CacheConfig {
|
|
13
|
+
/** Unique cache name used by runtime helpers and DB registration */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Optional ownership metadata for humans and runtime validation */
|
|
16
|
+
scope?: CacheScope;
|
|
17
|
+
/** Optional default TTL like "30m", "6h", or 86400 */
|
|
18
|
+
ttl?: string | number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cache(config: CacheConfig): CacheRegistration {
|
|
22
|
+
return {
|
|
23
|
+
name: config.name,
|
|
24
|
+
scope: config.scope,
|
|
25
|
+
ttl: config.ttl,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Usage Example (create a file like caches/cin7-products-snapshot.ts)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
//
|
|
33
|
+
// import { cache } from "./cache";
|
|
34
|
+
//
|
|
35
|
+
// export const cin7ProductsSnapshotCache = cache({
|
|
36
|
+
// name: "cin7-products-snapshot",
|
|
37
|
+
// scope: {
|
|
38
|
+
// plug: "cin7",
|
|
39
|
+
// resource: "products",
|
|
40
|
+
// flow: "cin7-to-pollinate-products-relay",
|
|
41
|
+
// },
|
|
42
|
+
// ttl: "6h",
|
|
43
|
+
// });
|
|
44
|
+
//
|
|
45
|
+
// Then register in your khotan config:
|
|
46
|
+
//
|
|
47
|
+
// import { cin7ProductsSnapshotCache } from "./caches/cin7-products-snapshot";
|
|
48
|
+
//
|
|
49
|
+
// const khotanData = khotan({
|
|
50
|
+
// adapter: drizzleAdapter(db),
|
|
51
|
+
// caches: [cin7ProductsSnapshotCache],
|
|
52
|
+
// plugs: [...],
|
|
53
|
+
// });
|
|
54
|
+
//
|
|
55
|
+
// Flow, relay, catch, and pass workflows can then use:
|
|
56
|
+
// await khotanCache(ctx, "cin7-products-snapshot").get("all-products");
|
|
57
|
+
// await khotanCache(ctx, "cin7-products-snapshot").set("all-products", payload);
|
|
58
|
+
// await khotanCache(ctx, "cin7-products-snapshot").delete("all-products");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Example: Catch
|
|
3
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
4
|
+
//
|
|
5
|
+
// Copy this file, rename it for your webhook source/event, and register the
|
|
6
|
+
// exported catch handler in {outputDir}/khotan.ts.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import { catchEvent, type CatchContext } from "./catch";
|
|
10
|
+
|
|
11
|
+
async function stripeInvoiceCatchWorkflow(ctx: CatchContext) {
|
|
12
|
+
"use workflow";
|
|
13
|
+
|
|
14
|
+
async function persistEvent() {
|
|
15
|
+
"use step";
|
|
16
|
+
console.log("Handling webhook event", {
|
|
17
|
+
eventType: ctx.eventType,
|
|
18
|
+
khotanRunId: ctx.khotanRunId,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Khotan already records webhook deliveries. Add app-specific side effects
|
|
22
|
+
// here, such as updating a local table or enqueueing downstream work.
|
|
23
|
+
console.log("Webhook payload", {
|
|
24
|
+
event: ctx.event,
|
|
25
|
+
headers: ctx.headers,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await persistEvent();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const stripeInvoiceCatch = catchEvent({
|
|
33
|
+
name: "stripe-invoices",
|
|
34
|
+
events: ["invoice.paid"],
|
|
35
|
+
workflow: stripeInvoiceCatchWorkflow,
|
|
36
|
+
});
|