mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-01-09 11:22:30 -06:00
Minor: move code around.
There's many things that could be broken up. Yet, let's at least move things into places where they're a bit more self-documenting.
This commit is contained in:
@@ -12,7 +12,7 @@ import { IndexPage } from "@/components/IndexPage";
|
||||
import { VerticalNavBar, HorizontalNavBar } from "@/components/NavBar";
|
||||
|
||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||||
import { $user } from "@/lib/fetch";
|
||||
import { $user } from "@/lib/client";
|
||||
import { createWindowWidth } from "@/lib/signals";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
|
||||
import { client } from "@/lib/fetch";
|
||||
import { client } from "@/lib/client";
|
||||
import { Toaster, showToast } from "@/components/ui/toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
TbSettings,
|
||||
} from "solid-icons/tb";
|
||||
|
||||
import { executeSql } from "@/lib/fetch";
|
||||
import { castToInteger } from "@/lib/convert";
|
||||
import { executeSql } from "@/lib/api/execute";
|
||||
import type { SqlValue } from "@/lib/value";
|
||||
|
||||
import { Header } from "@/components/Header";
|
||||
import { Card, CardContent, CardTitle } from "@/components/ui/card";
|
||||
@@ -240,3 +240,10 @@ export function IndexPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function castToInteger(value: SqlValue): bigint {
|
||||
if (typeof value === "object" && "Integer" in value) {
|
||||
return value.Integer;
|
||||
}
|
||||
throw Error(`Expected integer, got: ${value}`);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Version } from "@/components/Version";
|
||||
|
||||
import { createVersionInfoQuery } from "@/lib/info";
|
||||
import { createVersionInfoQuery } from "@/lib/api/info";
|
||||
|
||||
import logo from "@/assets/logo_104.webp";
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@ import {
|
||||
} from "@/components/FormFields";
|
||||
import { SafeSheet, SheetContainer } from "@/components/SafeSheet";
|
||||
|
||||
import { deleteUser, updateUser, fetchUsers } from "@/lib/user";
|
||||
import { deleteUser, updateUser, fetchUsers } from "@/lib/api/user";
|
||||
import { copyToClipboard } from "@/lib/utils";
|
||||
|
||||
import type { UpdateUserRequest } from "@bindings/UpdateUserRequest";
|
||||
import type { UserJson } from "@bindings/UserJson";
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import { createForm } from "@tanstack/solid-form";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SheetHeader, SheetTitle, SheetFooter } from "@/components/ui/sheet";
|
||||
|
||||
import {
|
||||
buildBoolFormField,
|
||||
buildTextFormField,
|
||||
buildSecretFormField,
|
||||
notEmptyValidator,
|
||||
} from "@/components/FormFields";
|
||||
import { createUser } from "@/lib/user";
|
||||
|
||||
import { createUser } from "@/lib/api/user";
|
||||
|
||||
import type { CreateUserRequest } from "@bindings/CreateUserRequest";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TbUser } from "solid-icons/tb";
|
||||
import { type User } from "trailbase";
|
||||
|
||||
import { urlSafeBase64ToUuid } from "@/lib/utils";
|
||||
import { client, $user } from "@/lib/fetch";
|
||||
import { client, $user } from "@/lib/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { client } from "@/lib/fetch";
|
||||
import { client } from "@/lib/client";
|
||||
|
||||
import { showToast } from "@/components/ui/toast";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
@@ -60,11 +60,11 @@ import type { QueryResponse } from "@bindings/QueryResponse";
|
||||
import type { ListSchemasResponse } from "@bindings/ListSchemasResponse";
|
||||
import type { SqlValue } from "@bindings/SqlValue";
|
||||
|
||||
import { createTableSchemaQuery } from "@/lib/table";
|
||||
import { executeSql, type ExecutionResult } from "@/lib/fetch";
|
||||
import { createTableSchemaQuery } from "@/lib/api/table";
|
||||
import { executeSql, type ExecutionResult } from "@/lib/api/execute";
|
||||
import { isNotNull } from "@/lib/schema";
|
||||
import { sqlValueToString } from "@/lib/value";
|
||||
import type { RowData } from "@/lib/convert";
|
||||
import type { ArrayRecord } from "@/lib/record";
|
||||
|
||||
function buildSchema(schemas: ListSchemasResponse): SQLNamespace {
|
||||
const schema: {
|
||||
@@ -136,13 +136,13 @@ function ResultView(props: {
|
||||
const isCached = () => props.response === undefined;
|
||||
const response = () => props.response ?? props.script.result;
|
||||
|
||||
function columnDefs(data: QueryResponse): ColumnDef<RowData, SqlValue>[] {
|
||||
function columnDefs(data: QueryResponse): ColumnDef<ArrayRecord, SqlValue>[] {
|
||||
return (data.columns ?? []).map((col, idx) => {
|
||||
const notNull = isNotNull(col.options);
|
||||
|
||||
const header = `${col.name} [${col.data_type}${notNull ? "" : "?"}]`;
|
||||
return {
|
||||
accessorFn: (row: RowData) => {
|
||||
accessorFn: (row: ArrayRecord) => {
|
||||
console.log(row);
|
||||
return sqlValueToString(row[idx]);
|
||||
},
|
||||
@@ -184,7 +184,7 @@ function ResultView(props: {
|
||||
{/* TODO: Enable pagination */}
|
||||
<DataTable
|
||||
columns={() => columnDefs(response()!.data!)}
|
||||
data={() => response()!.data!.rows as RowData[]}
|
||||
data={() => response()!.data!.rows as ArrayRecord[]}
|
||||
pagination={{
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Switch, Match, createMemo } from "solid-js";
|
||||
import { createTableSchemaQuery } from "@/lib/table";
|
||||
import { createTableSchemaQuery } from "@/lib/api/table";
|
||||
import { prettyFormatQualifiedName } from "@/lib/schema";
|
||||
|
||||
import { Header } from "@/components/Header";
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
NODE_WIDTH,
|
||||
LINE_HEIGHT,
|
||||
EDGE_COLOR,
|
||||
} from "@/components/ErdGraph";
|
||||
} from "@/components/erd/ErdGraph";
|
||||
|
||||
import {
|
||||
getForeignKey,
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
import { DataTable, safeParseInt } from "@/components/Table";
|
||||
import { FilterBar } from "@/components/FilterBar";
|
||||
|
||||
import { getLogs } from "@/lib/logs";
|
||||
import { getLogs } from "@/lib/api/logs";
|
||||
import { copyToClipboard } from "@/lib/utils";
|
||||
|
||||
import countriesGeoJSON from "@/assets/countries-110m.json";
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
OAuthProviderConfig,
|
||||
OAuthProviderId,
|
||||
} from "@proto/config";
|
||||
import { createConfigQuery, setConfig } from "@/lib/config";
|
||||
import { createConfigQuery, setConfig } from "@/lib/api/config";
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
import { createSetOnce } from "@/lib/signals";
|
||||
import { showSaveFileDialog, pathJoin, copyToClipboard } from "@/lib/utils";
|
||||
|
||||
@@ -44,8 +44,9 @@ import type { FormApiT } from "@/components/FormFields";
|
||||
import type { TestEmailRequest } from "@bindings/TestEmailRequest";
|
||||
|
||||
import { Config, EmailConfig, SmtpEncryption } from "@proto/config";
|
||||
import { createConfigQuery, setConfig } from "@/lib/config";
|
||||
import { $user, adminFetch } from "@/lib/fetch";
|
||||
import { createConfigQuery, setConfig } from "@/lib/api/config";
|
||||
import { $user } from "@/lib/client";
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
|
||||
import DEFAULT_EMAIL_VERIFICATION_SUBJECT from "@templates/default_email_verification_subject.txt?raw";
|
||||
import DEFAULT_EMAIL_VERIFICATION_BODY from "@templates/default_email_verification_body.html?raw";
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
|
||||
import { type FieldApiT, FieldInfo } from "@/components/FormFields";
|
||||
import { Config, JobsConfig, SystemJob } from "@proto/config";
|
||||
import { createConfigQuery, setConfig } from "@/lib/config";
|
||||
import { listJobs, runJob } from "@/lib/jobs";
|
||||
import { createConfigQuery, setConfig } from "@/lib/api/config";
|
||||
import { listJobs, runJob } from "@/lib/api/jobs";
|
||||
import type { Job } from "@bindings/Job";
|
||||
|
||||
const cronRegex =
|
||||
|
||||
@@ -34,8 +34,8 @@ import {
|
||||
createConfigQuery,
|
||||
setConfig,
|
||||
invalidateAllAdminQueries,
|
||||
} from "@/lib/config";
|
||||
import { createVersionInfoQuery } from "@/lib/info";
|
||||
} from "@/lib/api/config";
|
||||
import { createVersionInfoQuery } from "@/lib/api/info";
|
||||
|
||||
function ServerSettings(props: CommonProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { SheetHeader, SheetTitle, SheetFooter } from "@/components/ui/sheet";
|
||||
import { showToast } from "@/components/ui/toast";
|
||||
|
||||
import { alterIndex, createIndex } from "@/lib/table";
|
||||
import { alterIndex, createIndex } from "@/lib/api/table";
|
||||
import {
|
||||
buildTextFormField,
|
||||
buildBoolFormField,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { SheetHeader, SheetTitle, SheetFooter } from "@/components/ui/sheet";
|
||||
|
||||
import { createTable, alterTable } from "@/lib/table";
|
||||
import { createTable, alterTable } from "@/lib/api/table";
|
||||
import { randomName } from "@/lib/name";
|
||||
import {
|
||||
buildBoolFormField,
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
newDefaultColumn,
|
||||
primaryKeyPresets,
|
||||
} from "@/components/tables/CreateAlterColumnForm";
|
||||
import { invalidateConfig } from "@/lib/config";
|
||||
import { invalidateConfig } from "@/lib/api/config";
|
||||
|
||||
import type { Column } from "@bindings/Column";
|
||||
import type { Table } from "@bindings/Table";
|
||||
|
||||
@@ -25,9 +25,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import { literalDefault, shallowCopySqlValue } from "@/lib/convert";
|
||||
import type { Record } from "@/lib/convert";
|
||||
import { updateRow, insertRow } from "@/lib/row";
|
||||
import type { Record } from "@/lib/record";
|
||||
import { updateRow, insertRow } from "@/lib/api/row";
|
||||
import {
|
||||
sqlValueToString,
|
||||
getInteger,
|
||||
@@ -51,6 +50,7 @@ import {
|
||||
isPrimaryKeyColumn,
|
||||
isNullableColumn,
|
||||
getForeignKey,
|
||||
literalDefault,
|
||||
} from "@/lib/schema";
|
||||
|
||||
function buildDefaultRecord(schema: Table): Record {
|
||||
@@ -887,3 +887,15 @@ function validateInsertSqlValueFormField({
|
||||
// Pass validation.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shallowCopySqlValue(
|
||||
value: SqlValue | undefined,
|
||||
): SqlValue | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "Null") {
|
||||
return "Null";
|
||||
}
|
||||
return { ...value };
|
||||
}
|
||||
|
||||
@@ -62,9 +62,8 @@ import {
|
||||
RecordApiConfig,
|
||||
} from "@proto/config";
|
||||
|
||||
import { createConfigQuery, setConfig } from "@/lib/config";
|
||||
import { literalDefault } from "@/lib/convert";
|
||||
import { parseSqlExpression } from "@/lib/parse";
|
||||
import { createConfigQuery, setConfig } from "@/lib/api/config";
|
||||
import { parseSqlExpression } from "@/lib/api/parse";
|
||||
import {
|
||||
getColumns,
|
||||
getDefaultValue,
|
||||
@@ -72,9 +71,10 @@ import {
|
||||
isNotNull,
|
||||
isNullableColumn,
|
||||
isPrimaryKeyColumn,
|
||||
literalDefault,
|
||||
tableType,
|
||||
} from "@/lib/schema";
|
||||
import { client } from "@/lib/fetch";
|
||||
import { client } from "@/lib/client";
|
||||
import { fromHex } from "@/lib/utils";
|
||||
|
||||
import type { ForeignKey } from "@bindings/ForeignKey";
|
||||
|
||||
@@ -55,12 +55,12 @@ import {
|
||||
UploadedFiles,
|
||||
} from "@/components/tables/Files";
|
||||
|
||||
import { createConfigQuery, invalidateConfig } from "@/lib/config";
|
||||
import type { Record, RowData } from "@/lib/convert";
|
||||
import { hashSqlValue } from "@/lib/convert";
|
||||
import { createConfigQuery, invalidateConfig } from "@/lib/api/config";
|
||||
import type { Record, ArrayRecord } from "@/lib/record";
|
||||
import { hashSqlValue } from "@/lib/value";
|
||||
import { urlSafeBase64ToUuid, toHex } from "@/lib/utils";
|
||||
import { dropTable, dropIndex } from "@/lib/table";
|
||||
import { deleteRows, fetchRows } from "@/lib/row";
|
||||
import { dropTable, dropIndex } from "@/lib/api/table";
|
||||
import { deleteRows, fetchRows } from "@/lib/api/row";
|
||||
import {
|
||||
findPrimaryKeyColumnIndex,
|
||||
getForeignKey,
|
||||
@@ -92,7 +92,7 @@ export type SimpleSignal<T> = [get: () => T, set: (state: T) => void];
|
||||
const blobEncodings = ["base64", "hex", "mixed"] as const;
|
||||
type BlobEncoding = (typeof blobEncodings)[number];
|
||||
|
||||
function rowDataToRow(columns: Column[], row: RowData): Record {
|
||||
function rowDataToRow(columns: Column[], row: ArrayRecord): Record {
|
||||
const result: Record = {};
|
||||
for (let i = 0; i < row.length; ++i) {
|
||||
result[columns[i].name] = row[i];
|
||||
@@ -101,7 +101,7 @@ function rowDataToRow(columns: Column[], row: RowData): Record {
|
||||
}
|
||||
|
||||
function renderCell(
|
||||
context: CellContext<RowData, SqlValue>,
|
||||
context: CellContext<ArrayRecord, SqlValue>,
|
||||
tableName: QualifiedName,
|
||||
columns: Column[],
|
||||
pkIndex: number,
|
||||
@@ -486,7 +486,7 @@ function buildColumnDefs(
|
||||
pkColumnIndex: number,
|
||||
columns: Column[],
|
||||
blobEncoding: BlobEncoding,
|
||||
): ColumnDef<RowData, SqlValue>[] {
|
||||
): ColumnDef<ArrayRecord, SqlValue>[] {
|
||||
return columns.map((col, idx) => {
|
||||
const fk = getForeignKey(col.options);
|
||||
const notNull = isNotNull(col.options);
|
||||
@@ -510,12 +510,12 @@ function buildColumnDefs(
|
||||
},
|
||||
blobEncoding,
|
||||
),
|
||||
accessorFn: (row: RowData) => row[idx],
|
||||
} as ColumnDef<RowData, SqlValue>;
|
||||
accessorFn: (row: ArrayRecord) => row[idx],
|
||||
} as ColumnDef<ArrayRecord, SqlValue>;
|
||||
});
|
||||
}
|
||||
|
||||
function RowDataTable(props: {
|
||||
function ArrayRecordTable(props: {
|
||||
state: TableState;
|
||||
pagination: SimpleSignal<PaginationState>;
|
||||
filter: SimpleSignal<string | undefined>;
|
||||
@@ -600,14 +600,14 @@ function RowDataTable(props: {
|
||||
}}
|
||||
onRowClick={
|
||||
mutable()
|
||||
? (_idx: number, row: RowData) => {
|
||||
? (_idx: number, row: ArrayRecord) => {
|
||||
setEditRow(rowDataToRow(columns(), row));
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onRowSelection={
|
||||
mutable()
|
||||
? (rows: Row<RowData>[], value: boolean) => {
|
||||
? (rows: Row<ArrayRecord>[], value: boolean) => {
|
||||
const newSelection = new Map<string, SqlValue>(
|
||||
selectedRows(),
|
||||
);
|
||||
@@ -874,7 +874,7 @@ export function TablePane(props: {
|
||||
</Match>
|
||||
|
||||
<Match when={state.data}>
|
||||
<RowDataTable
|
||||
<ArrayRecordTable
|
||||
state={state.data!}
|
||||
pagination={[pagination, setPagination]}
|
||||
filter={[filter, setFilter]}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { SplitView } from "@/components/SplitView";
|
||||
import { SafeSheet } from "@/components/SafeSheet";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
import { createTableSchemaQuery } from "@/lib/table";
|
||||
import { createTableSchemaQuery } from "@/lib/api/table";
|
||||
import {
|
||||
hiddenTable,
|
||||
tableType,
|
||||
|
||||
44
crates/assets/js/admin/src/lib/api/execute.ts
Normal file
44
crates/assets/js/admin/src/lib/api/execute.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
|
||||
import type { QueryResponse } from "@bindings/QueryResponse";
|
||||
import type { QueryRequest } from "@bindings/QueryRequest";
|
||||
|
||||
export type ExecutionError = {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ExecutionResult = {
|
||||
query: string;
|
||||
timestamp: number;
|
||||
|
||||
data?: QueryResponse;
|
||||
error?: ExecutionError;
|
||||
};
|
||||
|
||||
export async function executeSql(sql: string): Promise<ExecutionResult> {
|
||||
const response = await adminFetch("/query", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
query: sql,
|
||||
} as QueryRequest),
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return {
|
||||
query: sql,
|
||||
timestamp: Date.now(),
|
||||
data: await response.json(),
|
||||
} as ExecutionResult;
|
||||
}
|
||||
|
||||
return {
|
||||
query: sql,
|
||||
timestamp: Date.now(),
|
||||
error: {
|
||||
code: response.status,
|
||||
message: await response.text(),
|
||||
} as ExecutionError,
|
||||
} as ExecutionResult;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
import { buildListSearchParams2 } from "@/lib/list";
|
||||
import { buildListSearchParams } from "@/lib/list";
|
||||
|
||||
import type { ListLogsResponse } from "@bindings/ListLogsResponse";
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function getLogs(
|
||||
filter?: string,
|
||||
cursor?: string | null,
|
||||
): Promise<ListLogsResponse> {
|
||||
const params = buildListSearchParams2({
|
||||
const params = buildListSearchParams({
|
||||
filter,
|
||||
pageSize,
|
||||
cursor,
|
||||
@@ -1,10 +1,10 @@
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
import { buildListSearchParams2 } from "@/lib/list";
|
||||
import { buildListSearchParams } from "@/lib/list";
|
||||
import {
|
||||
findPrimaryKeyColumnIndex,
|
||||
prettyFormatQualifiedName,
|
||||
} from "@/lib/schema";
|
||||
import type { Record } from "@/lib/convert";
|
||||
import type { Record } from "@/lib/record";
|
||||
|
||||
import type { Table } from "@bindings/Table";
|
||||
import type { InsertRowRequest } from "@bindings/InsertRowRequest";
|
||||
@@ -94,7 +94,7 @@ export async function fetchRows(
|
||||
pageIndex: number,
|
||||
cursor: string | null,
|
||||
): Promise<ListRowsResponse> {
|
||||
const params = buildListSearchParams2({
|
||||
const params = buildListSearchParams({
|
||||
filter,
|
||||
pageSize,
|
||||
pageIndex,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { adminFetch } from "@/lib/fetch";
|
||||
import { buildListSearchParams2 } from "@/lib/list";
|
||||
import { buildListSearchParams } from "@/lib/list";
|
||||
|
||||
import type { UpdateUserRequest } from "@bindings/UpdateUserRequest";
|
||||
import type { CreateUserRequest } from "@bindings/CreateUserRequest";
|
||||
@@ -32,7 +32,7 @@ export async function fetchUsers(
|
||||
pageSize: number,
|
||||
cursor: string | null,
|
||||
): Promise<ListUsersResponse> {
|
||||
const params = buildListSearchParams2({
|
||||
const params = buildListSearchParams({
|
||||
filter,
|
||||
pageSize,
|
||||
cursor,
|
||||
30
crates/assets/js/admin/src/lib/client.ts
Normal file
30
crates/assets/js/admin/src/lib/client.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { computed } from "nanostores";
|
||||
import { persistentAtom } from "@nanostores/persistent";
|
||||
import type { Client, Tokens, User } from "trailbase";
|
||||
import { initClient } from "trailbase";
|
||||
|
||||
const $tokens = persistentAtom<Tokens | null>("auth_tokens", null, {
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse,
|
||||
});
|
||||
export const $user = computed($tokens, (_tokens) => client.user());
|
||||
|
||||
function buildClient(): Client {
|
||||
// For our dev server setup we assume that a TrailBase instance is running at ":4000", otherwise
|
||||
// we query APIs relative to the origin's root path.
|
||||
const HOST = import.meta.env.DEV
|
||||
? new URL("http://localhost:4000")
|
||||
: undefined;
|
||||
const client = initClient(HOST, {
|
||||
tokens: $tokens.get() ?? undefined,
|
||||
onAuthChange: (c: Client, _user: User | undefined) => {
|
||||
$tokens.set(c.tokens() ?? null);
|
||||
},
|
||||
});
|
||||
|
||||
// This will also trigger a logout in case of 401.
|
||||
client.refreshAuthToken();
|
||||
|
||||
return client;
|
||||
}
|
||||
export const client = buildClient();
|
||||
@@ -1,64 +0,0 @@
|
||||
import { tryParseFloat, tryParseBigInt } from "@/lib/utils";
|
||||
import { unescapeLiteral, unescapeLiteralBlob } from "@/lib/schema";
|
||||
import type { SqlValue } from "@/lib/value";
|
||||
|
||||
import type { ColumnDataType } from "@bindings/ColumnDataType";
|
||||
|
||||
/// A record, i.e. row of SQL values (including "Null") or undefined (i.e.
|
||||
/// don't submit), keyed by column name. We use a map-like structure to allow
|
||||
/// for absence and avoid schema complexities and skew.
|
||||
export type Record = { [key: string]: SqlValue | undefined };
|
||||
|
||||
// An array representation of a single row.
|
||||
export type RowData = SqlValue[];
|
||||
|
||||
export function castToInteger(value: SqlValue): bigint {
|
||||
if (typeof value === "object" && "Integer" in value) {
|
||||
return value.Integer;
|
||||
}
|
||||
throw Error(`Expected integer, got: ${value}`);
|
||||
}
|
||||
|
||||
export function hashSqlValue(value: SqlValue): string {
|
||||
return `__${JSON.stringify(value)}`;
|
||||
}
|
||||
|
||||
export function shallowCopySqlValue(
|
||||
value: SqlValue | undefined,
|
||||
): SqlValue | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "Null") {
|
||||
return "Null";
|
||||
}
|
||||
return { ...value };
|
||||
}
|
||||
|
||||
export function literalDefault(
|
||||
type: ColumnDataType,
|
||||
value: string,
|
||||
): string | bigint | number | undefined {
|
||||
// Non literal if missing or function call, e.g. '(fun([col]))'.
|
||||
if (value === undefined || value.startsWith("(")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (type === "Blob") {
|
||||
// e.g. for X'abba' return "abba".
|
||||
const blob = unescapeLiteralBlob(value);
|
||||
if (blob !== undefined) {
|
||||
return blob;
|
||||
}
|
||||
return undefined;
|
||||
} else if (type === "Text") {
|
||||
// e.g. 'bar'.
|
||||
return unescapeLiteral(value);
|
||||
} else if (type === "Integer") {
|
||||
return tryParseBigInt(value);
|
||||
} else if (type === "Real") {
|
||||
return tryParseFloat(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,36 +1,4 @@
|
||||
import { computed } from "nanostores";
|
||||
import { persistentAtom } from "@nanostores/persistent";
|
||||
import type { Client, Tokens, User } from "trailbase";
|
||||
import { initClient } from "trailbase";
|
||||
|
||||
import type { QueryResponse } from "@bindings/QueryResponse";
|
||||
import type { QueryRequest } from "@bindings/QueryRequest";
|
||||
|
||||
const $tokens = persistentAtom<Tokens | null>("auth_tokens", null, {
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse,
|
||||
});
|
||||
export const $user = computed($tokens, (_tokens) => client.user());
|
||||
|
||||
function buildClient(): Client {
|
||||
// For our dev server setup we assume that a TrailBase instance is running at ":4000", otherwise
|
||||
// we query APIs relative to the origin's root path.
|
||||
const HOST = import.meta.env.DEV
|
||||
? new URL("http://localhost:4000")
|
||||
: undefined;
|
||||
const client = initClient(HOST, {
|
||||
tokens: $tokens.get() ?? undefined,
|
||||
onAuthChange: (c: Client, _user: User | undefined) => {
|
||||
$tokens.set(c.tokens() ?? null);
|
||||
},
|
||||
});
|
||||
|
||||
// This will also trigger a logout in case of 401.
|
||||
client.refreshAuthToken();
|
||||
|
||||
return client;
|
||||
}
|
||||
export const client = buildClient();
|
||||
import { client } from "@/lib/client";
|
||||
|
||||
type FetchOptions = RequestInit & {
|
||||
throwOnError?: boolean;
|
||||
@@ -51,43 +19,3 @@ export async function adminFetch(
|
||||
...init,
|
||||
});
|
||||
}
|
||||
|
||||
export type ExecutionError = {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ExecutionResult = {
|
||||
query: string;
|
||||
timestamp: number;
|
||||
|
||||
data?: QueryResponse;
|
||||
error?: ExecutionError;
|
||||
};
|
||||
|
||||
export async function executeSql(sql: string): Promise<ExecutionResult> {
|
||||
const response = await adminFetch("/query", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
query: sql,
|
||||
} as QueryRequest),
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return {
|
||||
query: sql,
|
||||
timestamp: Date.now(),
|
||||
data: await response.json(),
|
||||
} as ExecutionResult;
|
||||
}
|
||||
|
||||
return {
|
||||
query: sql,
|
||||
timestamp: Date.now(),
|
||||
error: {
|
||||
code: response.status,
|
||||
message: await response.text(),
|
||||
} as ExecutionError,
|
||||
} as ExecutionResult;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { parse, ExprGroup, Expr, JoinOp, SignOp } from "@/lib/fexpr";
|
||||
import { showToast } from "@/components/ui/toast";
|
||||
|
||||
export type ListArgs2 = {
|
||||
export type ListArgs = {
|
||||
filter: string | undefined | null;
|
||||
pageSize: number;
|
||||
pageIndex?: number;
|
||||
cursor: string | undefined | null;
|
||||
};
|
||||
|
||||
export function buildListSearchParams2({
|
||||
export function buildListSearchParams({
|
||||
filter,
|
||||
pageSize,
|
||||
pageIndex,
|
||||
cursor,
|
||||
}: ListArgs2): URLSearchParams {
|
||||
}: ListArgs): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filter) {
|
||||
|
||||
15
crates/assets/js/admin/src/lib/record.ts
Normal file
15
crates/assets/js/admin/src/lib/record.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { SqlValue } from "@/lib/value";
|
||||
|
||||
/// A record, i.e. row of SQL values (including "Null") or undefined (i.e.
|
||||
/// don't submit), keyed by column name.
|
||||
///
|
||||
/// We use this for insert/update. The map-like structure to allow / for absence
|
||||
/// and avoid schema complexities and skew. Values of `undefined` won't be
|
||||
/// serialized sent across the wire.
|
||||
export type Record = { [key: string]: SqlValue | undefined };
|
||||
|
||||
/// A(nother) record, , i.e. row of SQL values (including "Null").
|
||||
///
|
||||
/// We use this for reading/listing records. Every column is represented and is
|
||||
/// accessed by index.
|
||||
export type ArrayRecord = SqlValue[];
|
||||
@@ -1,4 +1,5 @@
|
||||
import { assert, TypeEqualityGuard } from "@/lib/value";
|
||||
import { tryParseBigInt, tryParseFloat } from "@/lib/utils";
|
||||
|
||||
import type { Column } from "@bindings/Column";
|
||||
import type { ColumnDataType } from "@bindings/ColumnDataType";
|
||||
@@ -50,6 +51,34 @@ export function setDefaultValue(
|
||||
return newOpts;
|
||||
}
|
||||
|
||||
export function literalDefault(
|
||||
type: ColumnDataType,
|
||||
value: string,
|
||||
): string | bigint | number | undefined {
|
||||
// Non literal if missing or function call, e.g. '(fun([col]))'.
|
||||
if (value === undefined || value.startsWith("(")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (type === "Blob") {
|
||||
// e.g. for X'abba' return "abba".
|
||||
const blob = unescapeLiteralBlob(value);
|
||||
if (blob !== undefined) {
|
||||
return blob;
|
||||
}
|
||||
return undefined;
|
||||
} else if (type === "Text") {
|
||||
// e.g. 'bar'.
|
||||
return unescapeLiteral(value);
|
||||
} else if (type === "Integer") {
|
||||
return tryParseBigInt(value);
|
||||
} else if (type === "Real") {
|
||||
return tryParseFloat(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function unpackCheckValue(col: ColumnOption): string | undefined {
|
||||
if (typeof col === "object" && "Check" in col) {
|
||||
return col.Check as string;
|
||||
|
||||
@@ -91,3 +91,7 @@ export function urlSafeBase64EncodedBlob(blob: Blob): string {
|
||||
|
||||
return urlSafeBase64Encode(Uint8Array.from(blob.Array));
|
||||
}
|
||||
|
||||
export function hashSqlValue(value: SqlValue): string {
|
||||
return `__${JSON.stringify(value)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user