@oxy-hq/sdk 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -4
- package/dist/function-context-D8eyZuw_.d.cts +720 -0
- package/dist/function-context-D8eyZuw_.d.cts.map +1 -0
- package/dist/function-context-D8eyZuw_.d.mts +720 -0
- package/dist/function-context-D8eyZuw_.d.mts.map +1 -0
- package/dist/index.cjs +217 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +499 -638
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +499 -638
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +215 -12
- package/dist/index.mjs.map +1 -1
- package/dist/ops.cjs +85 -0
- package/dist/ops.cjs.map +1 -0
- package/dist/ops.d.cts +61 -0
- package/dist/ops.d.cts.map +1 -0
- package/dist/ops.d.mts +61 -0
- package/dist/ops.d.mts.map +1 -0
- package/dist/ops.mjs +79 -0
- package/dist/ops.mjs.map +1 -0
- package/dist/{react-riTxd9ce.cjs → react-CkAQg9wB.cjs} +72 -7
- package/dist/react-CkAQg9wB.cjs.map +1 -0
- package/dist/{react-DqnINwTi.mjs → react-Cq3xULOr.mjs} +72 -7
- package/dist/react-Cq3xULOr.mjs.map +1 -0
- package/dist/{react-CLONxcnA.d.cts → react-D-Sf973d.d.cts} +94 -2
- package/dist/react-D-Sf973d.d.cts.map +1 -0
- package/dist/{react-CLONxcnA.d.mts → react-D-Sf973d.d.mts} +94 -2
- package/dist/react-D-Sf973d.d.mts.map +1 -0
- package/dist/shell.cjs +68 -4
- package/dist/shell.cjs.map +1 -1
- package/dist/shell.d.cts +62 -7
- package/dist/shell.d.cts.map +1 -1
- package/dist/shell.d.mts +62 -7
- package/dist/shell.d.mts.map +1 -1
- package/dist/shell.mjs +66 -5
- package/dist/shell.mjs.map +1 -1
- package/package.json +13 -2
- package/dist/react-CLONxcnA.d.cts.map +0 -1
- package/dist/react-CLONxcnA.d.mts.map +0 -1
- package/dist/react-DqnINwTi.mjs.map +0 -1
- package/dist/react-riTxd9ce.cjs.map +0 -1
package/dist/ops.d.cts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import { _ as OxyReach } from "./function-context-D8eyZuw_.cjs";
|
|
3
|
+
//#region src/ops/index.d.ts
|
|
4
|
+
/** What `ctx.user.reach` carries — one shape, declared once on the context type. */
|
|
5
|
+
type Reach = OxyReach;
|
|
6
|
+
/** The slice of `ctx` this module reads — a test hands in exactly this. */
|
|
7
|
+
interface ReachCtx {
|
|
8
|
+
user: {
|
|
9
|
+
/**
|
|
10
|
+
* Present on every invocation since SDK 2.12 / the operating graph. Typed
|
|
11
|
+
* optional here so a function can be written before its server is
|
|
12
|
+
* upgraded: an absent reach is the fail-closed answer, never the office.
|
|
13
|
+
*/
|
|
14
|
+
reach?: Reach | null;
|
|
15
|
+
appRole?: string;
|
|
16
|
+
kind?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The caller's reach. Synchronous — the platform decided it before the
|
|
21
|
+
* function ran — but `await reachOf(ctx)` still works, so code written against
|
|
22
|
+
* the app-side version keeps compiling.
|
|
23
|
+
*
|
|
24
|
+
* Absent reach (an older server) lands on nowhere. Not "system reaches
|
|
25
|
+
* everywhere, everyone else nowhere": a system invocation on an older server
|
|
26
|
+
* carries no reach either, and the one thing this module must never do is
|
|
27
|
+
* invent a wider answer than the platform gave.
|
|
28
|
+
*/
|
|
29
|
+
declare function reachOf(ctx: ReachCtx): Reach;
|
|
30
|
+
declare function reaches(reach: Reach, locationId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* For a writer naming a location: the 403 to return, or null to proceed.
|
|
33
|
+
*
|
|
34
|
+
* The message names the location and not the roster, and the code is stable:
|
|
35
|
+
* the client tells "you are not rostered here" from "no such location" (a 404
|
|
36
|
+
* the writers answer BEFORE this) and from "admin only" (a 403 with another
|
|
37
|
+
* code) without parsing English.
|
|
38
|
+
*/
|
|
39
|
+
declare function requireReach(ctx: ReachCtx, locationId: string): Response | null;
|
|
40
|
+
/**
|
|
41
|
+
* For a reader: a SQL expression that is true for rows in reach, over the
|
|
42
|
+
* given location column, binding through `params`.
|
|
43
|
+
*
|
|
44
|
+
* `TRUE` for a caller who reaches everywhere, so the statement reads the same
|
|
45
|
+
* either way. Otherwise the locations go in as ONE comma-joined text parameter
|
|
46
|
+
* unpacked by `string_to_array` — a location id is a uuid or a slug and can
|
|
47
|
+
* never contain the separator. An empty list becomes an empty array, so a
|
|
48
|
+
* caller assigned nowhere matches nothing rather than everything: the two
|
|
49
|
+
* states are different values, not one sentinel doing double duty.
|
|
50
|
+
*/
|
|
51
|
+
declare function predicate(reach: Reach, column: string, params: unknown[]): string;
|
|
52
|
+
/**
|
|
53
|
+
* The app-admin gate, decided in one place. `appRole` is derived by the
|
|
54
|
+
* platform (`Ring::AppAdmin`) and is the ONE field on `ctx.user` a function
|
|
55
|
+
* may gate on; `orgRole` and `teams` are there to explain, not to decide.
|
|
56
|
+
*/
|
|
57
|
+
declare function isAdmin(ctx: Pick<ReachCtx, "user">): boolean;
|
|
58
|
+
declare function adminOnly(what: string): Response;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { Reach, ReachCtx, adminOnly, isAdmin, predicate, reachOf, reaches, requireReach };
|
|
61
|
+
//# sourceMappingURL=ops.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops.d.cts","names":[],"sources":["../src/ops/index.ts"],"mappings":";;;;KAsBY,QAAQ;;UAGH;EACf;;;;;;IAME,QAAQ;IACR;IACA;;;;;;;;;;;;;iBAgBY,QAAQ,KAAK,WAAW;iBAMxB,QAAQ,OAAO,OAAO;;;;;;;;;iBAYtB,aAAa,KAAK,UAAU,qBAAqB;;;;;;;;;;;;iBAmBjD,UAAU,OAAO,OAAO,gBAAgB;;;;;;iBAgBxC,QAAQ,KAAK,KAAK;iBAIlB,UAAU,eAAe"}
|
package/dist/ops.d.mts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import { _ as OxyReach } from "./function-context-D8eyZuw_.mjs";
|
|
3
|
+
//#region src/ops/index.d.ts
|
|
4
|
+
/** What `ctx.user.reach` carries — one shape, declared once on the context type. */
|
|
5
|
+
type Reach = OxyReach;
|
|
6
|
+
/** The slice of `ctx` this module reads — a test hands in exactly this. */
|
|
7
|
+
interface ReachCtx {
|
|
8
|
+
user: {
|
|
9
|
+
/**
|
|
10
|
+
* Present on every invocation since SDK 2.12 / the operating graph. Typed
|
|
11
|
+
* optional here so a function can be written before its server is
|
|
12
|
+
* upgraded: an absent reach is the fail-closed answer, never the office.
|
|
13
|
+
*/
|
|
14
|
+
reach?: Reach | null;
|
|
15
|
+
appRole?: string;
|
|
16
|
+
kind?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The caller's reach. Synchronous — the platform decided it before the
|
|
21
|
+
* function ran — but `await reachOf(ctx)` still works, so code written against
|
|
22
|
+
* the app-side version keeps compiling.
|
|
23
|
+
*
|
|
24
|
+
* Absent reach (an older server) lands on nowhere. Not "system reaches
|
|
25
|
+
* everywhere, everyone else nowhere": a system invocation on an older server
|
|
26
|
+
* carries no reach either, and the one thing this module must never do is
|
|
27
|
+
* invent a wider answer than the platform gave.
|
|
28
|
+
*/
|
|
29
|
+
declare function reachOf(ctx: ReachCtx): Reach;
|
|
30
|
+
declare function reaches(reach: Reach, locationId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* For a writer naming a location: the 403 to return, or null to proceed.
|
|
33
|
+
*
|
|
34
|
+
* The message names the location and not the roster, and the code is stable:
|
|
35
|
+
* the client tells "you are not rostered here" from "no such location" (a 404
|
|
36
|
+
* the writers answer BEFORE this) and from "admin only" (a 403 with another
|
|
37
|
+
* code) without parsing English.
|
|
38
|
+
*/
|
|
39
|
+
declare function requireReach(ctx: ReachCtx, locationId: string): Response | null;
|
|
40
|
+
/**
|
|
41
|
+
* For a reader: a SQL expression that is true for rows in reach, over the
|
|
42
|
+
* given location column, binding through `params`.
|
|
43
|
+
*
|
|
44
|
+
* `TRUE` for a caller who reaches everywhere, so the statement reads the same
|
|
45
|
+
* either way. Otherwise the locations go in as ONE comma-joined text parameter
|
|
46
|
+
* unpacked by `string_to_array` — a location id is a uuid or a slug and can
|
|
47
|
+
* never contain the separator. An empty list becomes an empty array, so a
|
|
48
|
+
* caller assigned nowhere matches nothing rather than everything: the two
|
|
49
|
+
* states are different values, not one sentinel doing double duty.
|
|
50
|
+
*/
|
|
51
|
+
declare function predicate(reach: Reach, column: string, params: unknown[]): string;
|
|
52
|
+
/**
|
|
53
|
+
* The app-admin gate, decided in one place. `appRole` is derived by the
|
|
54
|
+
* platform (`Ring::AppAdmin`) and is the ONE field on `ctx.user` a function
|
|
55
|
+
* may gate on; `orgRole` and `teams` are there to explain, not to decide.
|
|
56
|
+
*/
|
|
57
|
+
declare function isAdmin(ctx: Pick<ReachCtx, "user">): boolean;
|
|
58
|
+
declare function adminOnly(what: string): Response;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { Reach, ReachCtx, adminOnly, isAdmin, predicate, reachOf, reaches, requireReach };
|
|
61
|
+
//# sourceMappingURL=ops.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops.d.mts","names":[],"sources":["../src/ops/index.ts"],"mappings":";;;;KAsBY,QAAQ;;UAGH;EACf;;;;;;IAME,QAAQ;IACR;IACA;;;;;;;;;;;;;iBAgBY,QAAQ,KAAK,WAAW;iBAMxB,QAAQ,OAAO,OAAO;;;;;;;;;iBAYtB,aAAa,KAAK,UAAU,qBAAqB;;;;;;;;;;;;iBAmBjD,UAAU,OAAO,OAAO,gBAAgB;;;;;;iBAgBxC,QAAQ,KAAK,KAAK;iBAIlB,UAAU,eAAe"}
|
package/dist/ops.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @oxy/sdk - TypeScript SDK for Oxy data platform
|
|
2
|
+
//#region src/ops/index.ts
|
|
3
|
+
const NOWHERE = {
|
|
4
|
+
everywhere: false,
|
|
5
|
+
via: null,
|
|
6
|
+
locations: []
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The caller's reach. Synchronous — the platform decided it before the
|
|
10
|
+
* function ran — but `await reachOf(ctx)` still works, so code written against
|
|
11
|
+
* the app-side version keeps compiling.
|
|
12
|
+
*
|
|
13
|
+
* Absent reach (an older server) lands on nowhere. Not "system reaches
|
|
14
|
+
* everywhere, everyone else nowhere": a system invocation on an older server
|
|
15
|
+
* carries no reach either, and the one thing this module must never do is
|
|
16
|
+
* invent a wider answer than the platform gave.
|
|
17
|
+
*/
|
|
18
|
+
function reachOf(ctx) {
|
|
19
|
+
const r = ctx.user.reach;
|
|
20
|
+
if (!r || typeof r !== "object" || !Array.isArray(r.locations)) return NOWHERE;
|
|
21
|
+
return {
|
|
22
|
+
everywhere: r.everywhere === true,
|
|
23
|
+
via: r.via ?? null,
|
|
24
|
+
locations: [...r.locations]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function reaches(reach, locationId) {
|
|
28
|
+
return reach.everywhere || reach.locations.includes(locationId);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* For a writer naming a location: the 403 to return, or null to proceed.
|
|
32
|
+
*
|
|
33
|
+
* The message names the location and not the roster, and the code is stable:
|
|
34
|
+
* the client tells "you are not rostered here" from "no such location" (a 404
|
|
35
|
+
* the writers answer BEFORE this) and from "admin only" (a 403 with another
|
|
36
|
+
* code) without parsing English.
|
|
37
|
+
*/
|
|
38
|
+
function requireReach(ctx, locationId) {
|
|
39
|
+
if (reaches(reachOf(ctx), locationId)) return null;
|
|
40
|
+
return Response.json({
|
|
41
|
+
error: `you are not rostered at ${locationId}`,
|
|
42
|
+
code: "OutOfReach",
|
|
43
|
+
locationId
|
|
44
|
+
}, { status: 403 });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* For a reader: a SQL expression that is true for rows in reach, over the
|
|
48
|
+
* given location column, binding through `params`.
|
|
49
|
+
*
|
|
50
|
+
* `TRUE` for a caller who reaches everywhere, so the statement reads the same
|
|
51
|
+
* either way. Otherwise the locations go in as ONE comma-joined text parameter
|
|
52
|
+
* unpacked by `string_to_array` — a location id is a uuid or a slug and can
|
|
53
|
+
* never contain the separator. An empty list becomes an empty array, so a
|
|
54
|
+
* caller assigned nowhere matches nothing rather than everything: the two
|
|
55
|
+
* states are different values, not one sentinel doing double duty.
|
|
56
|
+
*/
|
|
57
|
+
function predicate(reach, column, params) {
|
|
58
|
+
if (reach.everywhere) return "TRUE";
|
|
59
|
+
params.push(reach.locations.join(","));
|
|
60
|
+
return `${column}::text = ANY(string_to_array($${params.length}::text, ','))`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The app-admin gate, decided in one place. `appRole` is derived by the
|
|
64
|
+
* platform (`Ring::AppAdmin`) and is the ONE field on `ctx.user` a function
|
|
65
|
+
* may gate on; `orgRole` and `teams` are there to explain, not to decide.
|
|
66
|
+
*/
|
|
67
|
+
function isAdmin(ctx) {
|
|
68
|
+
return (ctx.user.appRole ?? "") === "admin";
|
|
69
|
+
}
|
|
70
|
+
function adminOnly(what) {
|
|
71
|
+
return Response.json({
|
|
72
|
+
error: `${what} needs app-admin standing`,
|
|
73
|
+
code: "AdminOnly"
|
|
74
|
+
}, { status: 403 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { adminOnly, isAdmin, predicate, reachOf, reaches, requireReach };
|
|
79
|
+
//# sourceMappingURL=ops.mjs.map
|
package/dist/ops.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops.mjs","names":[],"sources":["../src/ops/index.ts"],"sourcesContent":["// `@oxy-hq/sdk/ops` — the operating graph, applied inside an Oxy Function.\n//\n// The platform states where a caller may act (`ctx.user.reach`, derived from\n// their assignments — see `internal-docs/operating-graph.md` §3.3); this is\n// how a function applies it. Library code, versioned with the SDK, and\n// replaceable: an app that needs a tighter rule wraps `reachOf` and keeps the\n// rest. It may tighten, never widen — the reach the platform hands in is the\n// ceiling.\n//\n// Lifted from Store Ops's `functions/access.ts` (customer-apps #138) minus its\n// roster SQL: the roster is the platform's now, so the decision arrives on\n// `ctx.user` and nothing here reads a table.\n//\n// Two entry points. `requireReach` for a writer that names a location: the\n// 403 to return, or null. `predicate` for a reader: a SQL expression over the\n// statement's own location column, bound through the statement's parameter\n// list, so the scope lives in the WHERE — before the LIMIT, so a page is a\n// page of the right set.\n\nimport type { OxyReach } from \"../custom-app/function-context\";\n\n/** What `ctx.user.reach` carries — one shape, declared once on the context type. */\nexport type Reach = OxyReach;\n\n/** The slice of `ctx` this module reads — a test hands in exactly this. */\nexport interface ReachCtx {\n user: {\n /**\n * Present on every invocation since SDK 2.12 / the operating graph. Typed\n * optional here so a function can be written before its server is\n * upgraded: an absent reach is the fail-closed answer, never the office.\n */\n reach?: Reach | null;\n appRole?: string;\n kind?: string;\n };\n}\n\nconst NOWHERE: Reach = { everywhere: false, via: null, locations: [] };\n\n/**\n * The caller's reach. Synchronous — the platform decided it before the\n * function ran — but `await reachOf(ctx)` still works, so code written against\n * the app-side version keeps compiling.\n *\n * Absent reach (an older server) lands on nowhere. Not \"system reaches\n * everywhere, everyone else nowhere\": a system invocation on an older server\n * carries no reach either, and the one thing this module must never do is\n * invent a wider answer than the platform gave.\n */\nexport function reachOf(ctx: ReachCtx): Reach {\n const r = ctx.user.reach;\n if (!r || typeof r !== \"object\" || !Array.isArray(r.locations)) return NOWHERE;\n return { everywhere: r.everywhere === true, via: r.via ?? null, locations: [...r.locations] };\n}\n\nexport function reaches(reach: Reach, locationId: string): boolean {\n return reach.everywhere || reach.locations.includes(locationId);\n}\n\n/**\n * For a writer naming a location: the 403 to return, or null to proceed.\n *\n * The message names the location and not the roster, and the code is stable:\n * the client tells \"you are not rostered here\" from \"no such location\" (a 404\n * the writers answer BEFORE this) and from \"admin only\" (a 403 with another\n * code) without parsing English.\n */\nexport function requireReach(ctx: ReachCtx, locationId: string): Response | null {\n if (reaches(reachOf(ctx), locationId)) return null;\n return Response.json(\n { error: `you are not rostered at ${locationId}`, code: \"OutOfReach\", locationId },\n { status: 403 }\n );\n}\n\n/**\n * For a reader: a SQL expression that is true for rows in reach, over the\n * given location column, binding through `params`.\n *\n * `TRUE` for a caller who reaches everywhere, so the statement reads the same\n * either way. Otherwise the locations go in as ONE comma-joined text parameter\n * unpacked by `string_to_array` — a location id is a uuid or a slug and can\n * never contain the separator. An empty list becomes an empty array, so a\n * caller assigned nowhere matches nothing rather than everything: the two\n * states are different values, not one sentinel doing double duty.\n */\nexport function predicate(reach: Reach, column: string, params: unknown[]): string {\n if (reach.everywhere) return \"TRUE\";\n params.push(reach.locations.join(\",\"));\n // `::text` on the column: `string_to_array` yields `text[]`, and Postgres\n // will not compare a `uuid` column to it on its own (`uuid = text` has no\n // operator). The platform's location ids ARE uuids, so a tenant storing\n // them in the natural column type would otherwise get a reader that never\n // runs. A text column is unaffected.\n return `${column}::text = ANY(string_to_array($${params.length}::text, ','))`;\n}\n\n/**\n * The app-admin gate, decided in one place. `appRole` is derived by the\n * platform (`Ring::AppAdmin`) and is the ONE field on `ctx.user` a function\n * may gate on; `orgRole` and `teams` are there to explain, not to decide.\n */\nexport function isAdmin(ctx: Pick<ReachCtx, \"user\">): boolean {\n return (ctx.user.appRole ?? \"\") === \"admin\";\n}\n\nexport function adminOnly(what: string): Response {\n return Response.json(\n { error: `${what} needs app-admin standing`, code: \"AdminOnly\" },\n { status: 403 }\n );\n}\n"],"mappings":";;AAsCA,MAAM,UAAiB;CAAE,YAAY;CAAO,KAAK;CAAM,WAAW,CAAC;AAAE;;;;;;;;;;;AAYrE,SAAgB,QAAQ,KAAsB;CAC5C,MAAM,IAAI,IAAI,KAAK;CACnB,IAAI,CAAC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,EAAE,SAAS,GAAG,OAAO;CACvE,OAAO;EAAE,YAAY,EAAE,eAAe;EAAM,KAAK,EAAE,OAAO;EAAM,WAAW,CAAC,GAAG,EAAE,SAAS;CAAE;AAC9F;AAEA,SAAgB,QAAQ,OAAc,YAA6B;CACjE,OAAO,MAAM,cAAc,MAAM,UAAU,SAAS,UAAU;AAChE;;;;;;;;;AAUA,SAAgB,aAAa,KAAe,YAAqC;CAC/E,IAAI,QAAQ,QAAQ,GAAG,GAAG,UAAU,GAAG,OAAO;CAC9C,OAAO,SAAS,KACd;EAAE,OAAO,2BAA2B;EAAc,MAAM;EAAc;CAAW,GACjF,EAAE,QAAQ,IAAI,CAChB;AACF;;;;;;;;;;;;AAaA,SAAgB,UAAU,OAAc,QAAgB,QAA2B;CACjF,IAAI,MAAM,YAAY,OAAO;CAC7B,OAAO,KAAK,MAAM,UAAU,KAAK,GAAG,CAAC;CAMrC,OAAO,GAAG,OAAO,gCAAgC,OAAO,OAAO;AACjE;;;;;;AAOA,SAAgB,QAAQ,KAAsC;CAC5D,QAAQ,IAAI,KAAK,WAAW,QAAQ;AACtC;AAEA,SAAgB,UAAU,MAAwB;CAChD,OAAO,SAAS,KACd;EAAE,OAAO,GAAG,KAAK;EAA4B,MAAM;CAAY,GAC/D,EAAE,QAAQ,IAAI,CAChB;AACF"}
|
|
@@ -392,10 +392,25 @@ function validateFunctions(raw) {
|
|
|
392
392
|
}
|
|
393
393
|
fn.retries = retries;
|
|
394
394
|
}
|
|
395
|
+
if (value.webhook !== void 0) {
|
|
396
|
+
const w = value.webhook;
|
|
397
|
+
if (!isRecord(w)) throw new Error(`oxy-app.json: function "${fnName}" \`webhook\` must be an object`);
|
|
398
|
+
for (const key of ["secretVar", "signatureHeader"]) if (typeof w[key] !== "string" || !w[key].trim()) throw new Error(`oxy-app.json: function "${fnName}" \`webhook.${key}\` must be a non-empty string`);
|
|
399
|
+
const webhook = {
|
|
400
|
+
secretVar: w.secretVar,
|
|
401
|
+
signatureHeader: w.signatureHeader
|
|
402
|
+
};
|
|
403
|
+
if (w.encoding !== void 0) {
|
|
404
|
+
if (w.encoding !== "hex" && w.encoding !== "base64") throw new Error(`oxy-app.json: function "${fnName}" \`webhook.encoding\` must be "hex" or "base64"`);
|
|
405
|
+
webhook.encoding = w.encoding;
|
|
406
|
+
}
|
|
407
|
+
fn.webhook = webhook;
|
|
408
|
+
}
|
|
395
409
|
if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
|
|
396
410
|
const hasSchedule = fn.schedule !== void 0;
|
|
397
411
|
const hasAirway = fn.airwayStep !== void 0;
|
|
398
|
-
|
|
412
|
+
const hasWebhook = fn.webhook !== void 0;
|
|
413
|
+
if (!(fn.route ?? !(hasSchedule || hasAirway || hasWebhook)) && !hasSchedule && !hasAirway && !hasWebhook) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep/webhook`);
|
|
399
414
|
out[fnName] = fn;
|
|
400
415
|
}
|
|
401
416
|
return out;
|
|
@@ -649,6 +664,44 @@ async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
|
|
|
649
664
|
return p;
|
|
650
665
|
}
|
|
651
666
|
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/custom-app/traceparent.ts
|
|
669
|
+
const HEX = "0123456789abcdef";
|
|
670
|
+
function randomHex(bytes) {
|
|
671
|
+
const buf = new Uint8Array(bytes);
|
|
672
|
+
const c = globalThis.crypto;
|
|
673
|
+
if (c && typeof c.getRandomValues === "function") c.getRandomValues(buf);
|
|
674
|
+
else for (let i = 0; i < bytes; i++) buf[i] = Math.floor(Math.random() * 256);
|
|
675
|
+
let out = "";
|
|
676
|
+
for (let i = 0; i < bytes; i++) out += HEX[buf[i] >> 4] + HEX[buf[i] & 15];
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
/** Mint a fresh, sampled `traceparent`. All-zero ids are invalid per the spec;
|
|
680
|
+
* the loop guards the astronomically unlikely draw. */
|
|
681
|
+
function newTraceparent() {
|
|
682
|
+
let traceId = randomHex(16);
|
|
683
|
+
while (/^0+$/.test(traceId)) traceId = randomHex(16);
|
|
684
|
+
let spanId = randomHex(8);
|
|
685
|
+
while (/^0+$/.test(spanId)) spanId = randomHex(8);
|
|
686
|
+
return {
|
|
687
|
+
header: `00-${traceId}-${spanId}-01`,
|
|
688
|
+
traceId
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Stamp the ids of a failed invoke onto whatever was thrown, so the app (and
|
|
693
|
+
* the platform's error beacon, which reads `traceId`) can name the trace and
|
|
694
|
+
* the server-minted request id. Non-objects are returned untouched.
|
|
695
|
+
*/
|
|
696
|
+
function withInvocationIds(err, traceId, requestId) {
|
|
697
|
+
if (err && typeof err === "object") {
|
|
698
|
+
const target = err;
|
|
699
|
+
if (!target.traceId) target.traceId = traceId;
|
|
700
|
+
if (requestId && !target.requestId) target.requestId = requestId;
|
|
701
|
+
}
|
|
702
|
+
return err;
|
|
703
|
+
}
|
|
704
|
+
|
|
652
705
|
//#endregion
|
|
653
706
|
//#region src/custom-app/react.tsx
|
|
654
707
|
function defaultFetcher(input, init) {
|
|
@@ -921,9 +974,11 @@ function useFunction(name) {
|
|
|
921
974
|
error: null
|
|
922
975
|
}));
|
|
923
976
|
try {
|
|
977
|
+
const trace = newTraceparent();
|
|
924
978
|
const headers = {
|
|
925
979
|
"content-type": "application/json",
|
|
926
|
-
accept: "text/event-stream"
|
|
980
|
+
accept: "text/event-stream",
|
|
981
|
+
traceparent: trace.header
|
|
927
982
|
};
|
|
928
983
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
929
984
|
const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
|
|
@@ -932,8 +987,13 @@ function useFunction(name) {
|
|
|
932
987
|
headers,
|
|
933
988
|
body: JSON.stringify(body ?? {})
|
|
934
989
|
});
|
|
935
|
-
|
|
936
|
-
|
|
990
|
+
const requestId = resp.headers?.get?.("x-oxy-request-id") ?? null;
|
|
991
|
+
if (!resp.ok && resp.status !== 200) throw withInvocationIds(await apiErrorFromResponse(resp), trace.traceId, requestId);
|
|
992
|
+
try {
|
|
993
|
+
return await readFunctionSseStream(resp);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
throw withInvocationIds(err, trace.traceId, requestId);
|
|
996
|
+
}
|
|
937
997
|
});
|
|
938
998
|
setState({
|
|
939
999
|
data: result.value,
|
|
@@ -975,7 +1035,7 @@ function useFunction(name) {
|
|
|
975
1035
|
* inputs (e.g. a user-picked filter value) are available.
|
|
976
1036
|
*/
|
|
977
1037
|
function useSemanticQuery(input, opts = {}) {
|
|
978
|
-
const { projectId, fetcher } = useOxyApp();
|
|
1038
|
+
const { projectId, appId, fetcher } = useOxyApp();
|
|
979
1039
|
const enabled = opts.enabled !== false;
|
|
980
1040
|
const debug = opts.debug === true;
|
|
981
1041
|
const inputKey = react.useMemo(() => JSON.stringify(input), [input]);
|
|
@@ -1010,7 +1070,11 @@ function useSemanticQuery(input, opts = {}) {
|
|
|
1010
1070
|
measures: input.measures ?? [],
|
|
1011
1071
|
time_dimensions: input.time_dimensions ?? [],
|
|
1012
1072
|
filters: input.filters ?? [],
|
|
1013
|
-
...input.limit != null ? { limit: input.limit } : {}
|
|
1073
|
+
...input.limit != null ? { limit: input.limit } : {},
|
|
1074
|
+
...input.scope ? {
|
|
1075
|
+
scope: input.scope,
|
|
1076
|
+
...appId ? { app: appId } : {}
|
|
1077
|
+
} : {}
|
|
1014
1078
|
});
|
|
1015
1079
|
const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
|
|
1016
1080
|
fetcher(url, {
|
|
@@ -1048,6 +1112,7 @@ function useSemanticQuery(input, opts = {}) {
|
|
|
1048
1112
|
}, [
|
|
1049
1113
|
enabled,
|
|
1050
1114
|
projectId,
|
|
1115
|
+
appId,
|
|
1051
1116
|
inputKey,
|
|
1052
1117
|
debug,
|
|
1053
1118
|
nonce,
|
|
@@ -2439,4 +2504,4 @@ Object.defineProperty(exports, 'useTrackEvent', {
|
|
|
2439
2504
|
return useTrackEvent;
|
|
2440
2505
|
}
|
|
2441
2506
|
});
|
|
2442
|
-
//# sourceMappingURL=react-
|
|
2507
|
+
//# sourceMappingURL=react-CkAQg9wB.cjs.map
|