@rebasepro/studio 0.22.0 → 0.24.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/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
- package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
- package/dist/ApiKeysView-B66TdzhH.js +1738 -0
- package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
- package/dist/BackupsView-DmSMzAbT.js +368 -0
- package/dist/BackupsView-DmSMzAbT.js.map +1 -0
- package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
- package/dist/BranchesView-DP1a87E-.js.map +1 -0
- package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
- package/dist/CronJobsView-TDErHTuK.js.map +1 -0
- package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
- package/dist/JSEditor-B59pwg4c.js.map +1 -0
- package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
- package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
- package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
- package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
- package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
- package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
- package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
- package/dist/SQLEditor-1An7w0j6.js.map +1 -0
- package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
- package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
- package/dist/StorageView-CbsHuCBf.js.map +1 -0
- package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
- package/dist/components/ApiKeys/scope-words.d.ts +37 -0
- package/dist/components/ApiKeys/scopes.d.ts +113 -0
- package/dist/components/AuthSimulationSelector.d.ts +5 -1
- package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
- package/dist/components/RLSEditor/policyListing.d.ts +44 -0
- package/dist/components/RLSEditor/policyRules.d.ts +49 -0
- package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
- package/dist/components/load-failure.d.ts +1 -1
- package/dist/components/useRunAsUsers.d.ts +25 -0
- package/dist/index.es.js +11 -11
- package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
- package/dist/load-failure-Bd8lFIO7.js.map +1 -0
- package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
- package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +126 -36
- package/package.json +9 -9
- package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
- package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
- package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
- package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
- package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
- package/dist/BackupsView-DYTAD6aW.js +0 -253
- package/dist/BackupsView-DYTAD6aW.js.map +0 -1
- package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
- package/dist/CronJobsView-JwRjQCde.js.map +0 -1
- package/dist/JSEditor-qiuwmcvk.js.map +0 -1
- package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
- package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
- package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
- package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
- package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
- package/dist/StorageView-C52dSZNf.js.map +0 -1
- package/dist/components/ApiKeys/permissions.d.ts +0 -66
- package/dist/load-failure-C6OMJSGC.js.map +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PolicyExpression, PostgresPolicy, SecurityRule, SecurityRuleBase } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* The security-rule fields a {@link PostgresPolicy} describes, and the ones an
|
|
4
|
+
* edit carries over from the rule it changes.
|
|
5
|
+
*
|
|
6
|
+
* Wider than any one `SecurityRule` variant on purpose: an edit starts from the
|
|
7
|
+
* rule as written and deletes and assigns the fields that changed one at a
|
|
8
|
+
* time, and the variants are exclusive about which fields go together.
|
|
9
|
+
*/
|
|
10
|
+
export type PolicyRule = SecurityRuleBase & {
|
|
11
|
+
using?: string;
|
|
12
|
+
withCheck?: string;
|
|
13
|
+
ownerField?: string;
|
|
14
|
+
access?: "public";
|
|
15
|
+
condition?: PolicyExpression;
|
|
16
|
+
check?: PolicyExpression;
|
|
17
|
+
};
|
|
18
|
+
/** Why an edited policy could not be written back to a rule. */
|
|
19
|
+
export type PolicyEditRefusal =
|
|
20
|
+
/** Rebase adds it (the admin baseline, tenancy); no declared rule holds it. */
|
|
21
|
+
"generated"
|
|
22
|
+
/** It exists only in the database. */
|
|
23
|
+
| "not_declared"
|
|
24
|
+
/** Its rule covers several operations, and the policy is one of them. */
|
|
25
|
+
| "several_operations";
|
|
26
|
+
export type PolicyEdit = {
|
|
27
|
+
ok: true;
|
|
28
|
+
rules: PolicyRule[];
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
reason: PolicyEditRefusal;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Write an edit of one policy back to the declared rule it compiles from.
|
|
35
|
+
*
|
|
36
|
+
* The rule is found by the policy names it compiles to, not by `rule.name`:
|
|
37
|
+
* an unnamed rule's policy is `<table>_<op>_<hash>`, and a rule over several
|
|
38
|
+
* operations names each `<name>_<op>`. Matching on `name` missed both, saved
|
|
39
|
+
* the rules unchanged and reported success.
|
|
40
|
+
*
|
|
41
|
+
* A rule over several operations is refused rather than split: taking one
|
|
42
|
+
* operation out renames every policy the rule compiles to.
|
|
43
|
+
*
|
|
44
|
+
* @param declared The collection's `securityRules`, as written.
|
|
45
|
+
* @param effective Every rule the generator compiles for the table — the
|
|
46
|
+
* declared ones plus what Rebase adds — to tell a generated
|
|
47
|
+
* policy from one that exists only in the database.
|
|
48
|
+
*/
|
|
49
|
+
export declare function applyPolicyEdit(declared: readonly SecurityRule[], effective: readonly SecurityRule[], tableName: string, original: PostgresPolicy, edited: Partial<PostgresPolicy>): PolicyEdit;
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* What a host can ask of the editor outside its own key bindings.
|
|
4
|
+
*
|
|
5
|
+
* The toolbar's buttons need the selection that Cmd+Enter already reads: a
|
|
6
|
+
* button that runs the whole buffer while the shortcut beside it runs only the
|
|
7
|
+
* highlighted statement is two answers to one question.
|
|
8
|
+
*/
|
|
9
|
+
export interface MonacoEditorHandle {
|
|
10
|
+
/** The selected text, trimmed — `undefined` when nothing is selected. */
|
|
11
|
+
getSelectedText: () => string | undefined;
|
|
12
|
+
}
|
|
2
13
|
export type MonacoEditorProps = {
|
|
14
|
+
ref?: React.Ref<MonacoEditorHandle>;
|
|
3
15
|
value: string;
|
|
4
16
|
onChange: (value: string | undefined) => void;
|
|
5
17
|
onRun?: (selectedText?: string) => void;
|
|
@@ -15,4 +27,4 @@ export type MonacoEditorProps = {
|
|
|
15
27
|
}[];
|
|
16
28
|
}[]>;
|
|
17
29
|
};
|
|
18
|
-
export declare const MonacoEditor: ({ value, onChange, onRun, className, readOnly, autoFocus, schemas }: MonacoEditorProps) => React.JSX.Element;
|
|
30
|
+
export declare const MonacoEditor: ({ ref, value, onChange, onRun, className, readOnly, autoFocus, schemas }: MonacoEditorProps) => React.JSX.Element;
|
|
@@ -25,7 +25,6 @@ export declare const getLayoutedElements: (nodes: Node[], edges: Edge[]) => {
|
|
|
25
25
|
nodes: Node[];
|
|
26
26
|
edges: Edge[];
|
|
27
27
|
};
|
|
28
|
-
export declare const getTypeLabel: (type: string) => string;
|
|
29
28
|
export interface RelationEdgeData {
|
|
30
29
|
cardinality: "one" | "many";
|
|
31
30
|
direction: "owning" | "inverse";
|
|
@@ -3,6 +3,12 @@ import type { AdminCollection } from "@rebasepro/cms-types";
|
|
|
3
3
|
export interface ColumnInfo {
|
|
4
4
|
name: string;
|
|
5
5
|
type: string;
|
|
6
|
+
/**
|
|
7
|
+
* The type the collection declares — `string`, `number`, `date`… — or
|
|
8
|
+
* `FK` for a relation's key. Not the database's column type: the diagram
|
|
9
|
+
* is drawn from the collections, and a property's column type depends on
|
|
10
|
+
* more than its `type` (`columnType`, `isId`, integer validation).
|
|
11
|
+
*/
|
|
6
12
|
typeLabel: string;
|
|
7
13
|
isPrimaryKey: boolean;
|
|
8
14
|
isForeignKey: boolean;
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
export type LoadFailureKind =
|
|
36
36
|
/** The project's own rules, or the caller's own role, said no. Not an error. */
|
|
37
37
|
"denied"
|
|
38
|
-
/** We could not reach or read the source. */
|
|
38
|
+
/** We could not reach or read the source — including a session the server no longer accepts. */
|
|
39
39
|
| "unavailable";
|
|
40
40
|
export interface LoadFailure {
|
|
41
41
|
kind: LoadFailureKind;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type SelectableUser } from "@rebasepro/app";
|
|
2
|
+
export interface RunAsUsers {
|
|
3
|
+
/** The signed-in user first, then the users the latest search found. */
|
|
4
|
+
users: SelectableUser[];
|
|
5
|
+
/** The signed-in user, the picker's "self" row. */
|
|
6
|
+
currentUser: SelectableUser | null;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
/** Search the project's users on the server. */
|
|
9
|
+
onSearchTextChange: (searchText: string) => void;
|
|
10
|
+
/**
|
|
11
|
+
* How many users the latest search matched on the server, set only when
|
|
12
|
+
* that is more than the one page listed — so the picker can say so rather
|
|
13
|
+
* than present the page as everyone.
|
|
14
|
+
*/
|
|
15
|
+
totalCount?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The users a "Run as" picker offers.
|
|
19
|
+
*
|
|
20
|
+
* The signed-in user, and — for an administrator, the only caller the server
|
|
21
|
+
* lets run a request as someone else — the project's users, searched on the
|
|
22
|
+
* server. Disabled accounts are left out: nothing can run as them, and the
|
|
23
|
+
* server refuses them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useRunAsUsers(): RunAsUsers;
|
package/dist/index.es.js
CHANGED
|
@@ -550,17 +550,17 @@ function SyntaxHighlightedSnippet() {
|
|
|
550
550
|
}
|
|
551
551
|
//#endregion
|
|
552
552
|
//#region src/components/RebaseStudio.tsx
|
|
553
|
-
var SQLEditor = lazyChunk(() => import("./SQLEditor-
|
|
554
|
-
var JSEditor = lazyChunk(() => import("./JSEditor-
|
|
555
|
-
var RLSEditor = lazyChunk(() => import("./RLSEditor-
|
|
556
|
-
var StorageView = lazyChunk(() => import("./StorageView-
|
|
557
|
-
var CronJobsView = lazyChunk(() => import("./CronJobsView-
|
|
558
|
-
var SchemaVisualizer = lazyChunk(() => import("./SchemaVisualizer-
|
|
559
|
-
var BranchesView = lazyChunk(() => import("./BranchesView-
|
|
560
|
-
var BackupsView = lazyChunk(() => import("./BackupsView-
|
|
561
|
-
var ApiExplorer = lazyChunk(() => import("./ApiExplorer-
|
|
562
|
-
var LogsExplorer = lazyChunk(() => import("./LogsExplorer-
|
|
563
|
-
var ApiKeysView = lazyChunk(() => import("./ApiKeysView-
|
|
553
|
+
var SQLEditor = lazyChunk(() => import("./SQLEditor-1An7w0j6.js").then((m) => ({ default: m.SQLEditor })));
|
|
554
|
+
var JSEditor = lazyChunk(() => import("./JSEditor-B59pwg4c.js").then((m) => ({ default: m.JSEditor })));
|
|
555
|
+
var RLSEditor = lazyChunk(() => import("./RLSEditor-BlLLV6xs.js").then((m) => ({ default: m.RLSEditor })));
|
|
556
|
+
var StorageView = lazyChunk(() => import("./StorageView-CbsHuCBf.js").then((m) => ({ default: m.StorageView })));
|
|
557
|
+
var CronJobsView = lazyChunk(() => import("./CronJobsView-TDErHTuK.js").then((m) => ({ default: m.CronJobsView })));
|
|
558
|
+
var SchemaVisualizer = lazyChunk(() => import("./SchemaVisualizer-DCLVfA5t.js").then((m) => ({ default: m.SchemaVisualizer })));
|
|
559
|
+
var BranchesView = lazyChunk(() => import("./BranchesView-DP1a87E-.js").then((m) => ({ default: m.BranchesView })));
|
|
560
|
+
var BackupsView = lazyChunk(() => import("./BackupsView-DmSMzAbT.js").then((m) => ({ default: m.BackupsView })));
|
|
561
|
+
var ApiExplorer = lazyChunk(() => import("./ApiExplorer-CCtczj3F.js").then((m) => ({ default: m.ApiExplorer })));
|
|
562
|
+
var LogsExplorer = lazyChunk(() => import("./LogsExplorer-BKM4yfP8.js").then((m) => ({ default: m.LogsExplorer })));
|
|
563
|
+
var ApiKeysView = lazyChunk(() => import("./ApiKeysView-B66TdzhH.js").then((m) => ({ default: m.ApiKeysView })));
|
|
564
564
|
/**
|
|
565
565
|
* Declarative component to configure the Studio in Rebase.
|
|
566
566
|
* Renders nothing — purely registers config into the RebaseRegistry.
|
|
@@ -15,7 +15,7 @@ function statusOf(err) {
|
|
|
15
15
|
function classifyLoadFailure(err) {
|
|
16
16
|
const detail = err instanceof Error ? err.message : String(err);
|
|
17
17
|
const status = statusOf(err);
|
|
18
|
-
return status
|
|
18
|
+
return (status !== null ? status === 403 : /\bnot authori[sz]ed\b|\bforbidden\b|\bpermission denied\b/i.test(detail)) ? {
|
|
19
19
|
kind: "denied",
|
|
20
20
|
detail,
|
|
21
21
|
retryable: false
|
|
@@ -28,4 +28,4 @@ function classifyLoadFailure(err) {
|
|
|
28
28
|
//#endregion
|
|
29
29
|
export { classifyLoadFailure as t };
|
|
30
30
|
|
|
31
|
-
//# sourceMappingURL=load-failure-
|
|
31
|
+
//# sourceMappingURL=load-failure-Bd8lFIO7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-failure-Bd8lFIO7.js","names":[],"sources":["../src/components/load-failure.ts"],"sourcesContent":["/**\n * Why a Studio listing failed — and whether it is the platform's fault.\n *\n * ## Why this is a function\n *\n * The storage pane rendered one view for every failure: a red card titled\n * **\"Error loading storage\"** with whatever string came back underneath.\n *\n * On 2026-08-27 a customer opened the Files tab of a healthy project and read\n * \"Error loading storage — Not authorized for this object\". Nothing was broken.\n * That project's own `storageAuthorize` hook refuses an unscoped listing on\n * purpose — its comment says a `list` that is not scoped to a team \"is precisely\n * the enumeration this exists to stop\" — and the Files tab always opens at the\n * bucket root. The console took a policy working exactly as written and\n * presented it as a fault in the platform, which sends the reader hunting for a\n * problem that does not exist.\n *\n * A refusal and a failure need different words, a different colour, and a\n * different next step: one is \"your rule said no\", the other is \"we could not\n * ask\". Retry is an affordance for the second and noise on the first.\n *\n * ## Why it is shared\n *\n * Storage was the only pane that had learned this. Backups, Cron Jobs, API Keys\n * and Branches each caught the error, opened a snackbar that is gone in four\n * seconds, and left their list empty — so a reader who looked away, or arrived\n * after the toast, was told \"No backups found yet\" about a database that has\n * backups and an account that may not list them. One classifier and one view, so\n * the next pane inherits the distinction instead of rediscovering it.\n *\n * The view that renders one of these is `load-failure-view.tsx`. It is a\n * separate file so that this classifier — the part with the rules in it — can\n * be tested without dragging in `@rebasepro/app` and, behind it, react-router.\n */\n\nexport type LoadFailureKind =\n /** The project's own rules, or the caller's own role, said no. Not an error. */\n | \"denied\"\n /** We could not reach or read the source — including a session the server no longer accepts. */\n | \"unavailable\";\n\nexport interface LoadFailure {\n kind: LoadFailureKind;\n /** The message as the server gave it, never re-worded. */\n detail: string;\n /** Offering retry against a standing policy teaches people the button does nothing. */\n retryable: boolean;\n}\n\n/** HTTP status carried by whatever the SDK threw, if it carries one. */\nfunction statusOf(err: unknown): number | null {\n if (typeof err !== \"object\" || err === null) return null;\n for (const key of [\"status\", \"statusCode\", \"code\"]) {\n const v = (err as Record<string, unknown>)[key];\n if (typeof v === \"number\" && v >= 100 && v < 600) return v;\n }\n return null;\n}\n\nexport function classifyLoadFailure(err: unknown): LoadFailure {\n const detail = err instanceof Error ? err.message : String(err);\n const status = statusOf(err);\n\n // 403 is the rule's answer, and the only status that is. A 401 is the\n // session — expired, revoked, signed out elsewhere — and says nothing about\n // what the caller's role may do; a 5xx is the platform, even when its\n // message says \"permission denied\" (a missing GRANT is answered that way).\n // Both are failures to read, with Retry.\n //\n // The message match is only for an error that carries no status at all: an\n // SDK that flattens an error to a string is how a refusal reached the\n // screen unclassified.\n const denied = status !== null\n ? status === 403\n : /\\bnot authori[sz]ed\\b|\\bforbidden\\b|\\bpermission denied\\b/i.test(detail);\n\n return denied\n ? { kind: \"denied\", detail, retryable: false }\n : { kind: \"unavailable\", detail, retryable: true };\n}\n"],"mappings":";;AAkDA,SAAS,SAAS,KAA6B;CAC3C,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO;CACpD,KAAK,MAAM,OAAO;EAAC;EAAU;EAAc;CAAM,GAAG;EAChD,MAAM,IAAK,IAAgC;EAC3C,IAAI,OAAO,MAAM,YAAY,KAAK,OAAO,IAAI,KAAK,OAAO;CAC7D;CACA,OAAO;AACX;AAEA,SAAgB,oBAAoB,KAA2B;CAC3D,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC9D,MAAM,SAAS,SAAS,GAAG;CAe3B,QAJe,WAAW,OACpB,WAAW,MACX,6DAA6D,KAAK,MAAM,KAGxE;EAAE,MAAM;EAAU;EAAQ,WAAW;CAAM,IAC3C;EAAE,MAAM;EAAe;EAAQ,WAAW;CAAK;AACzD"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { UserSelectPopover, useRebaseClient, useRebaseContext } from "@rebasepro/app";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Button, KeyRoundIcon, Typography, iconSize } from "@rebasepro/ui";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { hasAdminRole } from "@rebasepro/types";
|
|
6
|
+
//#region src/components/AuthSimulationSelector.tsx
|
|
7
|
+
function AuthSimulationSelector({ authMode, setAuthMode, selectedUser, setSelectedUser, users, loading, onUserSearchTextChange, userTotalCount, currentUser }) {
|
|
8
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9
|
+
className: "flex flex-wrap items-center gap-3",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ jsx(KeyRoundIcon, {
|
|
12
|
+
size: iconSize.small,
|
|
13
|
+
className: "text-text-secondary dark:text-text-secondary-dark"
|
|
14
|
+
}),
|
|
15
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
16
|
+
variant: "caption",
|
|
17
|
+
className: "text-text-secondary dark:text-text-secondary-dark text-xs font-medium",
|
|
18
|
+
children: "Auth:"
|
|
19
|
+
}),
|
|
20
|
+
/* @__PURE__ */ jsx(Button, {
|
|
21
|
+
size: "small",
|
|
22
|
+
variant: authMode === "jwt" ? "outlined" : "outlined",
|
|
23
|
+
color: authMode === "jwt" ? "primary" : "neutral",
|
|
24
|
+
onClick: () => setAuthMode("jwt"),
|
|
25
|
+
className: "rounded-full !px-3 !py-1 !text-xs",
|
|
26
|
+
children: "JWT Token"
|
|
27
|
+
}),
|
|
28
|
+
/* @__PURE__ */ jsx(Button, {
|
|
29
|
+
size: "small",
|
|
30
|
+
variant: authMode === "none" ? "outlined" : "outlined",
|
|
31
|
+
color: authMode === "none" ? "error" : "neutral",
|
|
32
|
+
onClick: () => setAuthMode("none"),
|
|
33
|
+
className: "rounded-full !px-3 !py-1 !text-xs",
|
|
34
|
+
children: "No Auth"
|
|
35
|
+
}),
|
|
36
|
+
authMode === "jwt" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-hairline-strong mx-1" }),
|
|
38
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
39
|
+
variant: "caption",
|
|
40
|
+
className: "text-text-secondary dark:text-text-secondary-dark text-xs font-medium",
|
|
41
|
+
children: "Run as:"
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ jsx(UserSelectPopover, {
|
|
44
|
+
selectedUser,
|
|
45
|
+
onUserSelected: setSelectedUser,
|
|
46
|
+
users,
|
|
47
|
+
loading,
|
|
48
|
+
onSearchTextChange: onUserSearchTextChange,
|
|
49
|
+
totalCount: userTotalCount,
|
|
50
|
+
currentUser
|
|
51
|
+
})
|
|
52
|
+
] })
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/components/useRunAsUsers.ts
|
|
58
|
+
/** How many users one search of a "Run as" picker lists. */
|
|
59
|
+
var USER_PAGE_SIZE = 50;
|
|
60
|
+
/** The fields a picker row shows, copied off an account or the signed-in user. */
|
|
61
|
+
function toSelectableUser(user) {
|
|
62
|
+
return {
|
|
63
|
+
uid: user.uid,
|
|
64
|
+
displayName: user.displayName,
|
|
65
|
+
email: user.email,
|
|
66
|
+
photoURL: user.photoURL,
|
|
67
|
+
roles: user.roles
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The users a "Run as" picker offers.
|
|
72
|
+
*
|
|
73
|
+
* The signed-in user, and — for an administrator, the only caller the server
|
|
74
|
+
* lets run a request as someone else — the project's users, searched on the
|
|
75
|
+
* server. Disabled accounts are left out: nothing can run as them, and the
|
|
76
|
+
* server refuses them.
|
|
77
|
+
*/
|
|
78
|
+
function useRunAsUsers() {
|
|
79
|
+
const signedIn = useRebaseContext().authController?.user;
|
|
80
|
+
const client = useRebaseClient();
|
|
81
|
+
const adminApi = hasAdminRole(signedIn?.roles) ? client?.admin : void 0;
|
|
82
|
+
const [search, setSearch] = useState("");
|
|
83
|
+
const [listed, setListed] = useState([]);
|
|
84
|
+
const [totalCount, setTotalCount] = useState(void 0);
|
|
85
|
+
const [loading, setLoading] = useState(false);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!adminApi) return;
|
|
88
|
+
let cancelled = false;
|
|
89
|
+
setLoading(true);
|
|
90
|
+
adminApi.listUsersPaginated({
|
|
91
|
+
search: search || void 0,
|
|
92
|
+
limit: USER_PAGE_SIZE
|
|
93
|
+
}).then(({ users, total }) => {
|
|
94
|
+
if (cancelled) return;
|
|
95
|
+
setListed(users.filter((u) => !u.disabled).map(toSelectableUser));
|
|
96
|
+
setTotalCount(total > users.length ? total : void 0);
|
|
97
|
+
}).catch((err) => {
|
|
98
|
+
console.warn("Could not list users for the \"Run as\" picker", err);
|
|
99
|
+
if (!cancelled) {
|
|
100
|
+
setListed([]);
|
|
101
|
+
setTotalCount(void 0);
|
|
102
|
+
}
|
|
103
|
+
}).finally(() => {
|
|
104
|
+
if (!cancelled) setLoading(false);
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
};
|
|
109
|
+
}, [adminApi, search]);
|
|
110
|
+
const currentUser = useMemo(() => signedIn ? toSelectableUser(signedIn) : null, [signedIn]);
|
|
111
|
+
return {
|
|
112
|
+
users: useMemo(() => [...currentUser ? [currentUser] : [], ...listed.filter((u) => u.uid !== currentUser?.uid)], [currentUser, listed]),
|
|
113
|
+
currentUser,
|
|
114
|
+
loading,
|
|
115
|
+
onSearchTextChange: setSearch,
|
|
116
|
+
totalCount
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { AuthSimulationSelector as n, useRunAsUsers as t };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=useRunAsUsers-CLaAtXt6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRunAsUsers-CLaAtXt6.js","names":[],"sources":["../src/components/AuthSimulationSelector.tsx","../src/components/useRunAsUsers.ts"],"sourcesContent":["import React from \"react\";\nimport { Button, cls, iconSize, KeyRoundIcon, Typography } from \"@rebasepro/ui\";\nimport { UserSelectPopover, SelectableUser } from \"@rebasepro/app\";\n\nexport interface AuthSimulationSelectorProps {\n authMode: \"jwt\" | \"none\";\n setAuthMode: (mode: \"jwt\" | \"none\") => void;\n selectedUser: SelectableUser | null;\n setSelectedUser: (user: SelectableUser | null) => void;\n users: SelectableUser[];\n loading?: boolean;\n /** For a caller that searches `users` on the server — see `UserSelectPopover`. */\n onUserSearchTextChange?: (searchText: string) => void;\n /** How many users that search matched, when `users` is only its first page — see `UserSelectPopover`. */\n userTotalCount?: number;\n currentUser: SelectableUser | null;\n}\n\nexport function AuthSimulationSelector({\n authMode,\n setAuthMode,\n selectedUser,\n setSelectedUser,\n users,\n loading,\n onUserSearchTextChange,\n userTotalCount,\n currentUser\n}: AuthSimulationSelectorProps) {\n return (\n <div className=\"flex flex-wrap items-center gap-3\">\n <KeyRoundIcon size={iconSize.small} className=\"text-text-secondary dark:text-text-secondary-dark\" />\n <Typography\n variant=\"caption\"\n className=\"text-text-secondary dark:text-text-secondary-dark text-xs font-medium\"\n >\n Auth:\n </Typography>\n <Button\n size=\"small\"\n variant={authMode === \"jwt\" ? \"outlined\" : \"outlined\"}\n color={authMode === \"jwt\" ? \"primary\" : \"neutral\"}\n onClick={() => setAuthMode(\"jwt\")}\n className=\"rounded-full !px-3 !py-1 !text-xs\"\n >\n JWT Token\n </Button>\n <Button\n size=\"small\"\n variant={authMode === \"none\" ? \"outlined\" : \"outlined\"}\n color={authMode === \"none\" ? \"error\" : \"neutral\"}\n onClick={() => setAuthMode(\"none\")}\n className=\"rounded-full !px-3 !py-1 !text-xs\"\n >\n No Auth\n </Button>\n\n {authMode === \"jwt\" && (\n <>\n <div className=\"w-px h-4 bg-hairline-strong mx-1\" />\n <Typography\n variant=\"caption\"\n className=\"text-text-secondary dark:text-text-secondary-dark text-xs font-medium\"\n >\n Run as:\n </Typography>\n <UserSelectPopover\n selectedUser={selectedUser}\n onUserSelected={setSelectedUser}\n users={users}\n loading={loading}\n onSearchTextChange={onUserSearchTextChange}\n totalCount={userTotalCount}\n currentUser={currentUser}\n />\n </>\n )}\n </div>\n );\n}\n","import { useEffect, useMemo, useState } from \"react\";\nimport { useRebaseClient, useRebaseContext, type SelectableUser } from \"@rebasepro/app\";\nimport { hasAdminRole, type RebaseClient } from \"@rebasepro/types\";\n\n/** How many users one search of a \"Run as\" picker lists. */\nconst USER_PAGE_SIZE = 50;\n\n/** The fields a picker row shows, copied off an account or the signed-in user. */\nfunction toSelectableUser(user: SelectableUser): SelectableUser {\n return {\n uid: user.uid,\n displayName: user.displayName,\n email: user.email,\n photoURL: user.photoURL,\n roles: user.roles\n };\n}\n\nexport interface RunAsUsers {\n /** The signed-in user first, then the users the latest search found. */\n users: SelectableUser[];\n /** The signed-in user, the picker's \"self\" row. */\n currentUser: SelectableUser | null;\n loading: boolean;\n /** Search the project's users on the server. */\n onSearchTextChange: (searchText: string) => void;\n /**\n * How many users the latest search matched on the server, set only when\n * that is more than the one page listed — so the picker can say so rather\n * than present the page as everyone.\n */\n totalCount?: number;\n}\n\n/**\n * The users a \"Run as\" picker offers.\n *\n * The signed-in user, and — for an administrator, the only caller the server\n * lets run a request as someone else — the project's users, searched on the\n * server. Disabled accounts are left out: nothing can run as them, and the\n * server refuses them.\n */\nexport function useRunAsUsers(): RunAsUsers {\n const signedIn = useRebaseContext().authController?.user;\n const client = useRebaseClient<RebaseClient>();\n const adminApi = hasAdminRole(signedIn?.roles) ? client?.admin : undefined;\n\n const [search, setSearch] = useState(\"\");\n const [listed, setListed] = useState<SelectableUser[]>([]);\n const [totalCount, setTotalCount] = useState<number | undefined>(undefined);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n if (!adminApi) return;\n let cancelled = false;\n setLoading(true);\n adminApi.listUsersPaginated({ search: search || undefined, limit: USER_PAGE_SIZE })\n .then(({ users, total }) => {\n if (cancelled) return;\n setListed(users.filter((u) => !u.disabled).map(toSelectableUser));\n setTotalCount(total > users.length ? total : undefined);\n })\n .catch((err: unknown) => {\n console.warn(\"Could not list users for the \\\"Run as\\\" picker\", err);\n if (!cancelled) {\n setListed([]);\n setTotalCount(undefined);\n }\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n }, [adminApi, search]);\n\n const currentUser = useMemo((): SelectableUser | null => (signedIn ? toSelectableUser(signedIn) : null), [signedIn]);\n\n const users = useMemo(\n () => [...(currentUser ? [currentUser] : []), ...listed.filter((u) => u.uid !== currentUser?.uid)],\n [currentUser, listed]\n );\n\n return { users, currentUser, loading, onSearchTextChange: setSearch, totalCount };\n}\n"],"mappings":";;;;;;AAkBA,SAAgB,uBAAuB,EACnC,UACA,aACA,cACA,iBACA,OACA,SACA,wBACA,gBACA,eAC4B;CAC5B,OACI,qBAAC,OAAD;EAAK,WAAU;YAAf;GACI,oBAAC,cAAD;IAAc,MAAM,SAAS;IAAO,WAAU;GAAqD,CAAA;GACnG,oBAAC,YAAD;IACI,SAAQ;IACR,WAAU;cACb;GAEW,CAAA;GACZ,oBAAC,QAAD;IACI,MAAK;IACL,SAAS,aAAa,QAAQ,aAAa;IAC3C,OAAO,aAAa,QAAQ,YAAY;IACxC,eAAe,YAAY,KAAK;IAChC,WAAU;cACb;GAEO,CAAA;GACR,oBAAC,QAAD;IACI,MAAK;IACL,SAAS,aAAa,SAAS,aAAa;IAC5C,OAAO,aAAa,SAAS,UAAU;IACvC,eAAe,YAAY,MAAM;IACjC,WAAU;cACb;GAEO,CAAA;GAEP,aAAa,SACV,qBAAA,UAAA,EAAA,UAAA;IACI,oBAAC,OAAD,EAAK,WAAU,mCAAoC,CAAA;IACnD,oBAAC,YAAD;KACI,SAAQ;KACR,WAAU;eACb;IAEW,CAAA;IACZ,oBAAC,mBAAD;KACkB;KACd,gBAAgB;KACT;KACE;KACT,oBAAoB;KACpB,YAAY;KACC;IAChB,CAAA;GACH,EAAA,CAAA;EAEL;;AAEb;;;;AC1EA,IAAM,iBAAiB;;AAGvB,SAAS,iBAAiB,MAAsC;CAC5D,OAAO;EACH,KAAK,KAAK;EACV,aAAa,KAAK;EAClB,OAAO,KAAK;EACZ,UAAU,KAAK;EACf,OAAO,KAAK;CAChB;AACJ;;;;;;;;;AA0BA,SAAgB,gBAA4B;CACxC,MAAM,WAAW,iBAAiB,CAAC,CAAC,gBAAgB;CACpD,MAAM,SAAS,gBAA8B;CAC7C,MAAM,WAAW,aAAa,UAAU,KAAK,IAAI,QAAQ,QAAQ,KAAA;CAEjE,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,QAAQ,aAAa,SAA2B,CAAC,CAAC;CACzD,MAAM,CAAC,YAAY,iBAAiB,SAA6B,KAAA,CAAS;CAC1E,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAE5C,gBAAgB;EACZ,IAAI,CAAC,UAAU;EACf,IAAI,YAAY;EAChB,WAAW,IAAI;EACf,SAAS,mBAAmB;GAAE,QAAQ,UAAU,KAAA;GAAW,OAAO;EAAe,CAAC,CAAC,CAC9E,MAAM,EAAE,OAAO,YAAY;GACxB,IAAI,WAAW;GACf,UAAU,MAAM,QAAQ,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,gBAAgB,CAAC;GAChE,cAAc,QAAQ,MAAM,SAAS,QAAQ,KAAA,CAAS;EAC1D,CAAC,CAAC,CACD,OAAO,QAAiB;GACrB,QAAQ,KAAK,kDAAkD,GAAG;GAClE,IAAI,CAAC,WAAW;IACZ,UAAU,CAAC,CAAC;IACZ,cAAc,KAAA,CAAS;GAC3B;EACJ,CAAC,CAAC,CACD,cAAc;GACX,IAAI,CAAC,WAAW,WAAW,KAAK;EACpC,CAAC;EACL,aAAa;GACT,YAAY;EAChB;CACJ,GAAG,CAAC,UAAU,MAAM,CAAC;CAErB,MAAM,cAAc,cAAsC,WAAW,iBAAiB,QAAQ,IAAI,MAAO,CAAC,QAAQ,CAAC;CAOnH,OAAO;EAAE,OALK,cACJ,CAAC,GAAI,cAAc,CAAC,WAAW,IAAI,CAAC,GAAI,GAAG,OAAO,QAAQ,MAAM,EAAE,QAAQ,aAAa,GAAG,CAAC,GACjG,CAAC,aAAa,MAAM,CAGf;EAAO;EAAa;EAAS,oBAAoB;EAAW;CAAW;AACpF"}
|
|
@@ -1,52 +1,142 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
2
|
+
import type { SqlScriptResult } from "@rebasepro/types";
|
|
3
|
+
/**
|
|
4
|
+
* The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.
|
|
5
|
+
*
|
|
6
|
+
* Never `ANALYZE`: that executes the statement to time it, so explaining a
|
|
7
|
+
* `DELETE` deleted the rows — without the confirmation "Run" asks for.
|
|
8
|
+
*
|
|
9
|
+
* And never more than one statement. Sent as one simple query, `EXPLAIN (…)
|
|
10
|
+
* SELECT 1; DELETE …` explains the SELECT and *runs* the DELETE. Text the
|
|
11
|
+
* parser cannot read is still explained, as long as no `;` could be hiding a
|
|
12
|
+
* second statement in it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildExplainSql(sqlText: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the console's automatic `LIMIT` may be appended to `sqlText`: one
|
|
17
|
+
* top-level SELECT that has no limit of its own.
|
|
18
|
+
*
|
|
19
|
+
* Decided on the parsed statement, not the text. A search for the word SELECT
|
|
20
|
+
* also found it inside `INSERT INTO … SELECT` and `CREATE TABLE … AS SELECT`,
|
|
21
|
+
* which then copied a thousand rows and reported success, and at the start of
|
|
22
|
+
* a script whose last statement the appended `LIMIT` turned into a syntax
|
|
23
|
+
* error. Text the parser cannot read is left as written.
|
|
24
|
+
*/
|
|
25
|
+
export declare function acceptsAutoLimit(sqlText: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether "Run" asks before running `sqlText`: a statement in it deletes or
|
|
28
|
+
* updates without a WHERE of its own, drops or truncates.
|
|
29
|
+
*
|
|
30
|
+
* Decided per parsed statement, not on the text. A search of the text for the
|
|
31
|
+
* word WHERE found it in another statement or in a comment, so `DELETE FROM
|
|
32
|
+
* posts; SELECT * FROM posts WHERE id = 1` deleted every post without asking.
|
|
33
|
+
* Text the parser cannot read is confirmed whenever it names a destructive
|
|
34
|
+
* command.
|
|
35
|
+
*/
|
|
36
|
+
export declare function needsDestructiveConfirmation(sqlText: string): boolean;
|
|
2
37
|
/**
|
|
3
|
-
* A table
|
|
38
|
+
* A table as SQL: `"schema"."table"` when a schema is given, a bare `"table"`
|
|
39
|
+
* otherwise.
|
|
4
40
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
41
|
+
export declare function quoteTableName(tableName: string, schemaName?: string): string;
|
|
42
|
+
/** An identifier as SQL, whatever it holds: double-quoted, with `"` doubled. */
|
|
43
|
+
export declare function quoteIdentifier(identifier: string): string;
|
|
44
|
+
/** What the console knows about a result: its columns, and the tables they came from. */
|
|
45
|
+
export type ResultProvenance = Pick<SqlScriptResult, "columns" | "tables">;
|
|
46
|
+
/**
|
|
47
|
+
* The stored cell a result cell is, and the key that finds its row.
|
|
48
|
+
*/
|
|
49
|
+
export interface CellEditTarget {
|
|
50
|
+
schema: string;
|
|
51
|
+
table: string;
|
|
52
|
+
/** The table column the edited value is written to. */
|
|
53
|
+
column: string;
|
|
54
|
+
/** Each primary key column of the table, and the result column holding its value for the row. */
|
|
55
|
+
key: {
|
|
56
|
+
column: string;
|
|
57
|
+
resultColumn: string;
|
|
58
|
+
}[];
|
|
8
59
|
}
|
|
9
60
|
/**
|
|
10
|
-
*
|
|
61
|
+
* Why a cell cannot be edited: a translation key of the console's, and its
|
|
62
|
+
* parameters.
|
|
63
|
+
*/
|
|
64
|
+
export interface CellEditRefusal {
|
|
65
|
+
key: "studio_sql_edit_unreadable_query" | "studio_sql_edit_not_a_select" | "studio_sql_edit_duplicate_column" | "studio_sql_edit_computed_column" | "studio_sql_edit_not_a_table" | "studio_sql_edit_inherited_table" | "studio_sql_edit_no_primary_key" | "studio_sql_edit_key_missing" | "studio_sql_edit_table_read_twice";
|
|
66
|
+
params: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
export type CellEditResolution = {
|
|
69
|
+
target: CellEditTarget;
|
|
70
|
+
refusal?: undefined;
|
|
71
|
+
} | {
|
|
72
|
+
target?: undefined;
|
|
73
|
+
refusal: CellEditRefusal;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Which stored cell an edit of the result column `resultColumn` writes, and
|
|
77
|
+
* how its row is found again — or why no edit is safe.
|
|
78
|
+
*
|
|
79
|
+
* Decided on what the database reported, not on the query's text. Each
|
|
80
|
+
* column of the result says which table column it was read from, or that it
|
|
81
|
+
* was computed; a cell is editable only when it was read from a column of a
|
|
82
|
+
* table, every column of that table's primary key was read too, and each of
|
|
83
|
+
* them is the only column of the result with its name — a row holds one value
|
|
84
|
+
* per name.
|
|
85
|
+
*
|
|
86
|
+
* The text was the only evidence before, and it was wrong twice over. For
|
|
87
|
+
* `SELECT p.id, a.name FROM posts p JOIN authors a …` it took the post's `id`
|
|
88
|
+
* for the author's key and updated whichever author shared the post's id.
|
|
89
|
+
* For `SELECT id, lower(email) AS name FROM users` it wrote the edited text
|
|
90
|
+
* to `users.name`.
|
|
91
|
+
*
|
|
92
|
+
* The database reports a table, not which mention of it: in a self-join both
|
|
93
|
+
* sides are `authors`, and a key from one with a value from the other finds
|
|
94
|
+
* the wrong row. So the query is still read, for one thing — the table must
|
|
95
|
+
* be read once, and nothing it is read through may be read twice.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveCellEdit(sqlText: string, provenance: ResultProvenance, resultColumn: string): CellEditResolution;
|
|
98
|
+
/**
|
|
99
|
+
* A collection whose records a query's rows can be opened as.
|
|
11
100
|
*/
|
|
12
101
|
export interface ResolvedQueryCollection {
|
|
13
|
-
/**
|
|
102
|
+
/** The table, as the database named it. */
|
|
14
103
|
tableName: string;
|
|
15
|
-
|
|
16
|
-
tableAlias?: string;
|
|
104
|
+
schemaName: string;
|
|
17
105
|
/** The matched collection */
|
|
18
106
|
collection: AdminCollection;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
/** The result column name that holds the primary key for this table (e.g. "id", "author_id") */
|
|
22
|
-
pkColumn?: string;
|
|
107
|
+
/** The result column that holds the table's primary key. */
|
|
108
|
+
pkColumn: string;
|
|
23
109
|
}
|
|
24
110
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
111
|
+
* The collections a result's rows can be opened as: those whose table one of
|
|
112
|
+
* the result's columns was read from, with the table's (single-column) primary
|
|
113
|
+
* key read too, under a name no other column of the result has.
|
|
114
|
+
*
|
|
115
|
+
* Matched on the provenance the database reported. A text match took the
|
|
116
|
+
* result's `id` for every table of a join: `SELECT p.id, a.name FROM posts p
|
|
117
|
+
* JOIN authors a …` offered to open the author whose id was the post's.
|
|
27
118
|
*/
|
|
28
|
-
export declare function
|
|
29
|
-
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
119
|
+
export declare function resolveQueryCollections(provenance: ResultProvenance, collections: AdminCollection[]): ResolvedQueryCollection[];
|
|
30
120
|
/**
|
|
31
|
-
*
|
|
121
|
+
* The console's "Format SQL": runs of whitespace become one space, a comma is
|
|
122
|
+
* followed by one, and an `=` standing alone gets one on each side — in the
|
|
123
|
+
* code only.
|
|
32
124
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
125
|
+
* A literal, a quoted identifier and a comment are copied as they are: their
|
|
126
|
+
* text is data, and rewriting `'a,b'` to `'a, b'` changes the value the next
|
|
127
|
+
* Run writes. A line comment keeps the line break that ends it, or it would
|
|
128
|
+
* swallow the code after it. `>=`, `<=`, `!=`, `:=` and `=>` are operators of
|
|
129
|
+
* their own and are left whole.
|
|
130
|
+
*/
|
|
131
|
+
export declare function formatSql(sqlText: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* `sqlText` with the console's automatic `LIMIT` on it, or `null` when it
|
|
134
|
+
* takes none (see {@link acceptsAutoLimit}).
|
|
36
135
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
136
|
+
* The limit goes straight after the statement's last token — before a
|
|
137
|
+
* trailing comment, which would otherwise swallow it, and in place of a
|
|
138
|
+
* trailing `;`. A query that computes an aggregate is limited like any other:
|
|
139
|
+
* the limit applies to the rows it returns, so a `count(*)` still counts every
|
|
140
|
+
* row, and a window `count(*) OVER ()` is no longer a way past the cap.
|
|
39
141
|
*/
|
|
40
|
-
export declare function
|
|
41
|
-
export interface PKMapping {
|
|
42
|
-
/** The actual column name in the database table */
|
|
43
|
-
dbColumn: string;
|
|
44
|
-
/** The column name as it appears in the query result set (may be aliased) */
|
|
45
|
-
resultColumn: string;
|
|
46
|
-
}
|
|
47
|
-
export interface TableAndPKResult {
|
|
48
|
-
tableName?: string;
|
|
49
|
-
primaryKeys?: PKMapping[];
|
|
50
|
-
error?: string;
|
|
51
|
-
}
|
|
52
|
-
export declare function determineTableAndPK(sqlString: string, columnKey: string, schemas: Record<string, TableInfo[]>): TableAndPKResult;
|
|
142
|
+
export declare function withAutoLimit(sqlText: string, limit: number): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Rebase Studio — the SQL editor, schema visualiser, RLS policy editor and API explorer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -35,19 +35,19 @@
|
|
|
35
35
|
"pgsql-ast-parser": "12.0.2",
|
|
36
36
|
"prism-react-renderer": "^2.4.1",
|
|
37
37
|
"react-dropzone": "^19.1.1",
|
|
38
|
-
"@rebasepro/cms-types": "0.
|
|
39
|
-
"@rebasepro/
|
|
40
|
-
"@rebasepro/
|
|
41
|
-
"@rebasepro/
|
|
42
|
-
"@rebasepro/utils": "0.
|
|
43
|
-
"@rebasepro/
|
|
44
|
-
"@rebasepro/types": "0.
|
|
38
|
+
"@rebasepro/cms-types": "0.24.0",
|
|
39
|
+
"@rebasepro/app": "0.24.0",
|
|
40
|
+
"@rebasepro/client": "0.24.0",
|
|
41
|
+
"@rebasepro/ui": "0.24.0",
|
|
42
|
+
"@rebasepro/utils": "0.24.0",
|
|
43
|
+
"@rebasepro/common": "0.24.0",
|
|
44
|
+
"@rebasepro/types": "0.24.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": "^19.2.7",
|
|
48
48
|
"react-dom": "^19.2.7",
|
|
49
49
|
"react-router": "^8.3.0",
|
|
50
|
-
"@rebasepro/cms": "0.
|
|
50
|
+
"@rebasepro/cms": "0.24.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"@rebasepro/cms": {
|