@protobase/cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/bin/protobase.mjs +4 -0
- package/dist/build/config-bundle.js +70 -0
- package/dist/build/deploy-bundle.js +42 -0
- package/dist/build/entry-code.js +23 -0
- package/dist/build/layout-check.js +50 -0
- package/dist/build/packages/binary-platform.js +81 -0
- package/dist/build/packages/copy-packages.js +59 -0
- package/dist/build/packages/host-packages.js +37 -0
- package/dist/build/packages/native-package.js +22 -0
- package/dist/build/packages/package-dir.js +32 -0
- package/dist/build/packages/package-tree.js +53 -0
- package/dist/build/packages/targets.js +38 -0
- package/dist/build/serve-runtime.js +16 -0
- package/dist/build/ui-bundle.js +33 -0
- package/dist/build/vite-bundle.js +36 -0
- package/dist/bundle/manifest.js +35 -0
- package/dist/db/connect.js +18 -0
- package/dist/db/introspect.js +47 -0
- package/dist/db/keys.js +15 -0
- package/dist/db/model.js +4 -0
- package/dist/db/queries.js +62 -0
- package/dist/dev/check-database.js +38 -0
- package/dist/dev/create-db.js +13 -0
- package/dist/dev/entry.d.ts +4 -0
- package/dist/dev/entry.js +32 -0
- package/dist/dev/env.js +15 -0
- package/dist/dev/hosts.js +4 -0
- package/dist/dev/project.js +39 -0
- package/dist/dev/run.js +88 -0
- package/dist/doctor/check.js +76 -0
- package/dist/doctor/load-config.js +10 -0
- package/dist/doctor/run.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/infer/check-values.js +14 -0
- package/dist/infer/compat.js +17 -0
- package/dist/infer/default-column.js +12 -0
- package/dist/infer/defaults.js +28 -0
- package/dist/infer/names.js +28 -0
- package/dist/infer/proposal.js +68 -0
- package/dist/infer/render-field.js +26 -0
- package/dist/infer/tenant.js +5 -0
- package/dist/infer/type-map.js +52 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.js +84 -0
- package/dist/module-file.js +6 -0
- package/dist/package.js +4 -0
- package/dist/project/admin-app.js +9 -0
- package/dist/project/convention.js +8 -0
- package/dist/project/import-project-file.js +6 -0
- package/dist/project/pg-db.js +11 -0
- package/dist/project/project-ui.js +17 -0
- package/dist/scaffold/data-file.js +44 -0
- package/dist/scaffold/drift.js +30 -0
- package/dist/scaffold/existing-config.js +95 -0
- package/dist/scaffold/index-file.js +18 -0
- package/dist/scaffold/plan.js +128 -0
- package/dist/scaffold/project.js +13 -0
- package/dist/scaffold/prompt.js +70 -0
- package/dist/scaffold/resource-names.js +19 -0
- package/dist/scaffold/resource-plan.js +33 -0
- package/dist/scaffold/run.js +53 -0
- package/dist/scaffold/targets.js +15 -0
- package/dist/scaffold/ui-fields.js +30 -0
- package/dist/scaffold/ui-file.js +40 -0
- package/dist/serve/app.js +30 -0
- package/dist/serve/bundle-version.js +16 -0
- package/dist/serve/env.js +13 -0
- package/dist/serve/host-module-ids.js +21 -0
- package/dist/serve/host-modules.js +49 -0
- package/dist/serve/lifecycle.js +39 -0
- package/dist/serve/load-bundle.js +17 -0
- package/dist/serve/main.d.ts +1 -0
- package/dist/serve/main.js +13 -0
- package/dist/serve/serve-bundle.js +21 -0
- package/dist/serve/serve-dir.js +15 -0
- package/dist/serve/signals.js +11 -0
- package/dist/serve/static-site.js +46 -0
- package/dist/users/commands.js +31 -0
- package/dist/users/load-auth.js +15 -0
- package/dist/users/manage.js +21 -0
- package/dist/users/password.js +64 -0
- package/dist/users/register.js +94 -0
- package/dist/users/ttl.js +16 -0
- package/dist/version/compatibility.js +31 -0
- package/dist/version/version.js +5 -0
- package/package.json +55 -3
- package/README.md +0 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { findPackageDir, readPackageJson } from "./package-dir.js";
|
|
2
|
+
import { isPlatformPackage, packageTargets } from "./targets.js";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
//#region src/build/packages/package-tree.ts
|
|
5
|
+
const lookupPath = (dest, name) => {
|
|
6
|
+
const parts = dest.split("/node_modules/");
|
|
7
|
+
return [...parts.map((_, i) => [...parts.slice(0, parts.length - i), name].join("/node_modules/")), name];
|
|
8
|
+
};
|
|
9
|
+
const runsOnTarget = (json) => !isPlatformPackage(json) || packageTargets(json).length > 0;
|
|
10
|
+
const packageTree = async (imports) => {
|
|
11
|
+
const placed = /* @__PURE__ */ new Map();
|
|
12
|
+
const tree = [];
|
|
13
|
+
const place = (pkg) => {
|
|
14
|
+
placed.set(pkg.dest, pkg.dir);
|
|
15
|
+
tree.push(pkg);
|
|
16
|
+
};
|
|
17
|
+
for (const imported of imports) {
|
|
18
|
+
const { name } = imported;
|
|
19
|
+
const dir = await realpath(imported.dir);
|
|
20
|
+
const there = placed.get(name);
|
|
21
|
+
if (there !== void 0 && there !== dir) throw new Error(`The config imports two installs of ${name}: ${there} and ${dir}`);
|
|
22
|
+
const json = await readPackageJson(dir);
|
|
23
|
+
if (there === void 0 && runsOnTarget(json)) place({
|
|
24
|
+
name,
|
|
25
|
+
dir,
|
|
26
|
+
dest: name,
|
|
27
|
+
json
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
for (let i = 0; i < tree.length; i++) {
|
|
31
|
+
const parent = tree[i];
|
|
32
|
+
const optional = parent.json.optionalDependencies ?? {};
|
|
33
|
+
for (const name of /* @__PURE__ */ new Set([...Object.keys(parent.json.dependencies ?? {}), ...Object.keys(optional)])) {
|
|
34
|
+
const dir = await findPackageDir(name, parent.dir);
|
|
35
|
+
if (!dir && name in optional) continue;
|
|
36
|
+
if (!dir) throw new Error(`${name}, which ${parent.name} needs, is not installed`);
|
|
37
|
+
const json = await readPackageJson(dir);
|
|
38
|
+
if (!runsOnTarget(json)) continue;
|
|
39
|
+
const candidates = lookupPath(parent.dest, name);
|
|
40
|
+
const found = candidates.find((dest) => placed.has(dest));
|
|
41
|
+
if (found !== void 0 && placed.get(found) === dir) continue;
|
|
42
|
+
place({
|
|
43
|
+
name,
|
|
44
|
+
dir,
|
|
45
|
+
dest: found === void 0 ? name : candidates[0],
|
|
46
|
+
json
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tree;
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { packageTree };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/build/packages/targets.ts
|
|
2
|
+
const bundleTargets = ["linux-x64", "linux-arm64"];
|
|
3
|
+
const isTarget = (platform) => bundleTargets.includes(platform);
|
|
4
|
+
const runImageLibraries = /* @__PURE__ */ new Set([
|
|
5
|
+
"ld-linux-x86-64.so.2",
|
|
6
|
+
"ld-linux-aarch64.so.1",
|
|
7
|
+
"libc.so.6",
|
|
8
|
+
"libm.so.6",
|
|
9
|
+
"libmvec.so.1",
|
|
10
|
+
"libdl.so.2",
|
|
11
|
+
"libpthread.so.0",
|
|
12
|
+
"librt.so.1",
|
|
13
|
+
"libresolv.so.2",
|
|
14
|
+
"libutil.so.1",
|
|
15
|
+
"libanl.so.1",
|
|
16
|
+
"libnsl.so.1",
|
|
17
|
+
"libBrokenLocale.so.1",
|
|
18
|
+
"libthread_db.so.1",
|
|
19
|
+
"libc_malloc_debug.so.0",
|
|
20
|
+
"libnss_compat.so.2",
|
|
21
|
+
"libnss_dns.so.2",
|
|
22
|
+
"libnss_files.so.2",
|
|
23
|
+
"libnss_hesiod.so.2",
|
|
24
|
+
"libstdc++.so.6",
|
|
25
|
+
"libgcc_s.so.1"
|
|
26
|
+
]);
|
|
27
|
+
const allows = (list, value) => {
|
|
28
|
+
if (!list?.length) return true;
|
|
29
|
+
if (list.includes(`!${value}`)) return false;
|
|
30
|
+
return list.includes(value) || list.every((item) => item.startsWith("!"));
|
|
31
|
+
};
|
|
32
|
+
const isPlatformPackage = (json) => Boolean(json.os?.length || json.cpu?.length || json.libc?.length);
|
|
33
|
+
const packageTargets = (json) => bundleTargets.filter((target) => {
|
|
34
|
+
const [os, cpu] = target.split("-");
|
|
35
|
+
return allows(json.os, os) && allows(json.cpu, cpu) && allows(json.libc, "glibc");
|
|
36
|
+
});
|
|
37
|
+
//#endregion
|
|
38
|
+
export { bundleTargets, isPlatformPackage, isTarget, packageTargets, runImageLibraries };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { moduleFile } from "../module-file.js";
|
|
2
|
+
import { viteBundle } from "./vite-bundle.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
//#region src/build/serve-runtime.ts
|
|
5
|
+
const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
6
|
+
const serveMain = moduleFile("../serve/main", import.meta.url);
|
|
7
|
+
const buildServeRuntime = async ({ outFile }) => {
|
|
8
|
+
await viteBundle({
|
|
9
|
+
root: cliRoot,
|
|
10
|
+
entry: serveMain,
|
|
11
|
+
outFile,
|
|
12
|
+
plugins: []
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { buildServeRuntime };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { adminAppDir } from "../project/admin-app.js";
|
|
2
|
+
import { projectUiPlugin } from "../project/project-ui.js";
|
|
3
|
+
import { build } from "vite";
|
|
4
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
+
import react from "@vitejs/plugin-react";
|
|
6
|
+
//#region src/build/ui-bundle.ts
|
|
7
|
+
const buildUiBundle = async ({ outDir, projectDir }) => {
|
|
8
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
9
|
+
process.env.NODE_ENV = "production";
|
|
10
|
+
await build({
|
|
11
|
+
configFile: false,
|
|
12
|
+
root: adminAppDir,
|
|
13
|
+
base: "/",
|
|
14
|
+
mode: "production",
|
|
15
|
+
publicDir: false,
|
|
16
|
+
logLevel: "warn",
|
|
17
|
+
plugins: [
|
|
18
|
+
projectUiPlugin(projectDir),
|
|
19
|
+
react(),
|
|
20
|
+
tailwindcss()
|
|
21
|
+
],
|
|
22
|
+
build: {
|
|
23
|
+
outDir,
|
|
24
|
+
emptyOutDir: true,
|
|
25
|
+
chunkSizeWarningLimit: 4096
|
|
26
|
+
}
|
|
27
|
+
}).finally(() => {
|
|
28
|
+
if (nodeEnv === void 0) delete process.env.NODE_ENV;
|
|
29
|
+
else process.env.NODE_ENV = nodeEnv;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { buildUiBundle };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { build } from "vite";
|
|
3
|
+
//#region src/build/vite-bundle.ts
|
|
4
|
+
const viteBundle = async ({ root, entry, outFile, plugins }) => {
|
|
5
|
+
await build({
|
|
6
|
+
configFile: false,
|
|
7
|
+
root,
|
|
8
|
+
publicDir: false,
|
|
9
|
+
logLevel: "warn",
|
|
10
|
+
plugins,
|
|
11
|
+
oxc: { jsx: { development: false } },
|
|
12
|
+
ssr: {
|
|
13
|
+
noExternal: true,
|
|
14
|
+
target: "node"
|
|
15
|
+
},
|
|
16
|
+
build: {
|
|
17
|
+
ssr: entry,
|
|
18
|
+
outDir: path.dirname(outFile),
|
|
19
|
+
emptyOutDir: false,
|
|
20
|
+
copyPublicDir: false,
|
|
21
|
+
minify: false,
|
|
22
|
+
target: "esnext",
|
|
23
|
+
rolldownOptions: {
|
|
24
|
+
external: ["pg-native"],
|
|
25
|
+
output: {
|
|
26
|
+
entryFileNames: path.basename(outFile),
|
|
27
|
+
format: "es",
|
|
28
|
+
codeSplitting: false,
|
|
29
|
+
banner: "// @bun"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { viteBundle };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
//#region src/bundle/manifest.ts
|
|
4
|
+
const manifestFile = "protobase.bundle.json";
|
|
5
|
+
const isApiPath = (api, pathname) => api.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
|
|
6
|
+
const bundleBasePath = "/api/v1";
|
|
7
|
+
const bundleLayout = {
|
|
8
|
+
server: "protobase.config.js",
|
|
9
|
+
public: "public",
|
|
10
|
+
spa: "index.html",
|
|
11
|
+
api: ["/api"]
|
|
12
|
+
};
|
|
13
|
+
const bundleNodeModules = "node_modules";
|
|
14
|
+
const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
15
|
+
const parseManifest = (value, file) => {
|
|
16
|
+
const manifest = value;
|
|
17
|
+
if (manifest?.version !== 2) throw new Error(`${file} is not a version 2 bundle manifest; rebuild it with \`protobase build\``);
|
|
18
|
+
const { server, public: publicDir, api, spa, nodeModules } = manifest;
|
|
19
|
+
if (typeof server !== "string" || typeof publicDir !== "string" || typeof spa !== "string" || !isStringList(api)) throw new Error(`${file} needs server, public and spa paths and an api list`);
|
|
20
|
+
if (nodeModules !== void 0 && typeof nodeModules !== "string") throw new Error(`${file} has a nodeModules that is no path`);
|
|
21
|
+
return {
|
|
22
|
+
version: 2,
|
|
23
|
+
server,
|
|
24
|
+
public: publicDir,
|
|
25
|
+
api,
|
|
26
|
+
spa,
|
|
27
|
+
...nodeModules === void 0 ? {} : { nodeModules }
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const readManifest = async (bundleDir) => {
|
|
31
|
+
const file = path.join(bundleDir, manifestFile);
|
|
32
|
+
return parseManifest(JSON.parse(await readFile(file, "utf8")), file);
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { bundleBasePath, bundleLayout, bundleNodeModules, isApiPath, manifestFile, parseManifest, readManifest };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
//#region src/db/connect.ts
|
|
3
|
+
const connect = (url, options = {}) => postgres(url, {
|
|
4
|
+
max: 1,
|
|
5
|
+
...options.connectTimeout && { connect_timeout: options.connectTimeout },
|
|
6
|
+
onnotice: () => {},
|
|
7
|
+
connection: { default_transaction_read_only: true }
|
|
8
|
+
});
|
|
9
|
+
const resolveConnectionUrl = (arg, envName) => {
|
|
10
|
+
if (arg && envName) throw new Error("Pass either a connection string or --env, not both");
|
|
11
|
+
if (arg) return arg;
|
|
12
|
+
if (!envName) throw new Error("Pass a connection string or --env <VARIABLE>");
|
|
13
|
+
const value = process.env[envName];
|
|
14
|
+
if (!value) throw new Error(`Environment variable ${envName} is not set`);
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { connect, resolveConnectionUrl };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { tableKey } from "./model.js";
|
|
2
|
+
import { selectChecks, selectColumns, selectForeignKeys, selectIndexes } from "./queries.js";
|
|
3
|
+
//#region src/db/introspect.ts
|
|
4
|
+
const byTable = (rows) => {
|
|
5
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const row of rows) {
|
|
7
|
+
const key = tableKey({
|
|
8
|
+
schema: row.schema,
|
|
9
|
+
name: row.table
|
|
10
|
+
});
|
|
11
|
+
groups.set(key, [...groups.get(key) ?? [], row]);
|
|
12
|
+
}
|
|
13
|
+
return groups;
|
|
14
|
+
};
|
|
15
|
+
const introspect = async (sql) => {
|
|
16
|
+
const [columns, foreignKeys, checks, indexes] = await Promise.all([
|
|
17
|
+
selectColumns(sql),
|
|
18
|
+
selectForeignKeys(sql),
|
|
19
|
+
selectChecks(sql),
|
|
20
|
+
selectIndexes(sql)
|
|
21
|
+
]);
|
|
22
|
+
const fks = byTable(foreignKeys);
|
|
23
|
+
const chks = byTable(checks);
|
|
24
|
+
const idxs = byTable(indexes);
|
|
25
|
+
return [...byTable(columns)].map(([key, rows]) => ({
|
|
26
|
+
schema: rows[0].schema,
|
|
27
|
+
name: rows[0].table,
|
|
28
|
+
columns: rows.map(({ schema, table, enumValues, defaultExpr, ...column }) => ({
|
|
29
|
+
...column,
|
|
30
|
+
enumValues: enumValues ?? [],
|
|
31
|
+
...defaultExpr !== null && { defaultExpr }
|
|
32
|
+
})),
|
|
33
|
+
foreignKeys: (fks.get(key) ?? []).map(({ columns, refSchema, refTable, refColumns }) => ({
|
|
34
|
+
columns,
|
|
35
|
+
refSchema,
|
|
36
|
+
refTable,
|
|
37
|
+
refColumns
|
|
38
|
+
})),
|
|
39
|
+
checks: (chks.get(key) ?? []).map(({ columns, definition }) => ({
|
|
40
|
+
columns,
|
|
41
|
+
definition
|
|
42
|
+
})),
|
|
43
|
+
indexes: (idxs.get(key) ?? []).map(({ schema, table, ...index }) => index)
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { introspect };
|
package/dist/db/keys.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/db/keys.ts
|
|
2
|
+
const plainColumns = (columns) => columns.every((column) => column !== null) ? columns : void 0;
|
|
3
|
+
const keyColumns = (table) => {
|
|
4
|
+
const primary = table.indexes.find((index) => index.primary);
|
|
5
|
+
if (primary) return plainColumns(primary.columns);
|
|
6
|
+
const notNull = new Set(table.columns.filter((column) => column.notNull).map((column) => column.name));
|
|
7
|
+
return table.indexes.filter((index) => index.unique && !index.partial).map((index) => plainColumns(index.columns)).find((columns) => columns && columns.every((column) => notNull.has(column)));
|
|
8
|
+
};
|
|
9
|
+
const isUniqueSet = (table, columns) => table.indexes.some((index) => {
|
|
10
|
+
const indexed = plainColumns(index.columns);
|
|
11
|
+
return index.unique && !index.partial && indexed && indexed.every((column) => columns.includes(column));
|
|
12
|
+
});
|
|
13
|
+
const leadingIndex = (table, column, tenantColumn) => table.indexes.find((index) => !index.partial && (index.columns[0] === column || tenantColumn !== void 0 && index.columns[0] === tenantColumn && index.columns[1] === column));
|
|
14
|
+
//#endregion
|
|
15
|
+
export { isUniqueSet, keyColumns, leadingIndex };
|
package/dist/db/model.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/db/queries.ts
|
|
2
|
+
const userTables = `
|
|
3
|
+
c.relkind in ('r', 'p')
|
|
4
|
+
and n.nspname not in ('pg_catalog', 'information_schema', 'pg_toast')
|
|
5
|
+
and n.nspname not like 'pg_temp%'
|
|
6
|
+
`;
|
|
7
|
+
const selectColumns = (sql) => sql`
|
|
8
|
+
select n.nspname as schema, c.relname as table, a.attname as name, a.attnum::int as position,
|
|
9
|
+
bt.typname as "typeName", format_type(a.atttypid, a.atttypmod) as formatted,
|
|
10
|
+
(bt.typtype = 'e') as "isEnum",
|
|
11
|
+
(select array_agg(e.enumlabel::text order by e.enumsortorder) from pg_enum e where e.enumtypid = bt.oid) as "enumValues",
|
|
12
|
+
a.attnotnull as "notNull", (a.attidentity <> '') as identity, (a.attidentity = 'a') as "identityAlways", (a.attgenerated <> '') as generated,
|
|
13
|
+
pg_get_expr(d.adbin, d.adrelid) as "defaultExpr"
|
|
14
|
+
from pg_attribute a
|
|
15
|
+
join pg_class c on c.oid = a.attrelid
|
|
16
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
17
|
+
join pg_type t on t.oid = a.atttypid
|
|
18
|
+
join pg_type bt on bt.oid = case when t.typtype = 'd' then t.typbasetype else t.oid end
|
|
19
|
+
left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
|
|
20
|
+
where ${sql.unsafe(userTables)} and a.attnum > 0 and not a.attisdropped
|
|
21
|
+
order by n.nspname, c.relname, a.attnum
|
|
22
|
+
`;
|
|
23
|
+
const selectForeignKeys = (sql) => sql`
|
|
24
|
+
select n.nspname as schema, c.relname as table,
|
|
25
|
+
(select array_agg(a.attname::text order by k.ord) from unnest(con.conkey) with ordinality k(attnum, ord)
|
|
26
|
+
join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum) as columns,
|
|
27
|
+
rn.nspname as "refSchema", rc.relname as "refTable",
|
|
28
|
+
(select array_agg(a.attname::text order by k.ord) from unnest(con.confkey) with ordinality k(attnum, ord)
|
|
29
|
+
join pg_attribute a on a.attrelid = con.confrelid and a.attnum = k.attnum) as "refColumns"
|
|
30
|
+
from pg_constraint con
|
|
31
|
+
join pg_class c on c.oid = con.conrelid
|
|
32
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
33
|
+
join pg_class rc on rc.oid = con.confrelid
|
|
34
|
+
join pg_namespace rn on rn.oid = rc.relnamespace
|
|
35
|
+
where con.contype = 'f' and ${sql.unsafe(userTables)}
|
|
36
|
+
`;
|
|
37
|
+
const selectChecks = (sql) => sql`
|
|
38
|
+
select n.nspname as schema, c.relname as table,
|
|
39
|
+
(select array_agg(a.attname::text order by k.ord) from unnest(con.conkey) with ordinality k(attnum, ord)
|
|
40
|
+
join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum) as columns,
|
|
41
|
+
pg_get_constraintdef(con.oid) as definition
|
|
42
|
+
from pg_constraint con
|
|
43
|
+
join pg_class c on c.oid = con.conrelid
|
|
44
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
45
|
+
where con.contype = 'c' and con.conkey is not null and ${sql.unsafe(userTables)}
|
|
46
|
+
`;
|
|
47
|
+
const selectIndexes = (sql) => sql`
|
|
48
|
+
select n.nspname as schema, c.relname as table, ic.relname as name,
|
|
49
|
+
(select array_agg(case when k.attnum = 0 then null else a.attname::text end order by k.ord)
|
|
50
|
+
from unnest(i.indkey::int2[]) with ordinality k(attnum, ord)
|
|
51
|
+
left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum
|
|
52
|
+
where k.ord <= i.indnkeyatts) as columns,
|
|
53
|
+
i.indisunique as unique, i.indisprimary as primary, (i.indpred is not null) as partial, am.amname as method
|
|
54
|
+
from pg_index i
|
|
55
|
+
join pg_class c on c.oid = i.indrelid
|
|
56
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
57
|
+
join pg_class ic on ic.oid = i.indexrelid
|
|
58
|
+
join pg_am am on am.oid = ic.relam
|
|
59
|
+
where i.indisvalid and ${sql.unsafe(userTables)}
|
|
60
|
+
`;
|
|
61
|
+
//#endregion
|
|
62
|
+
export { selectChecks, selectColumns, selectForeignKeys, selectIndexes };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { connect } from "../db/connect.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
//#region src/dev/check-database.ts
|
|
5
|
+
const describeTarget = (url) => {
|
|
6
|
+
const { hostname, port, pathname } = new URL(url);
|
|
7
|
+
return `${hostname}:${port || 5432}${pathname}`;
|
|
8
|
+
};
|
|
9
|
+
const startHint = async (projectDir) => {
|
|
10
|
+
const manifest = await readFile(path.join(projectDir, "package.json"), "utf8").catch((error) => {
|
|
11
|
+
if (error.code === "ENOENT") return void 0;
|
|
12
|
+
throw error;
|
|
13
|
+
});
|
|
14
|
+
if (!manifest) return "";
|
|
15
|
+
const { name, scripts } = JSON.parse(manifest);
|
|
16
|
+
if (!scripts?.["db:up"]) return "";
|
|
17
|
+
return `\nStart it with: pnpm --filter ${name} db:up`;
|
|
18
|
+
};
|
|
19
|
+
const reasons = {
|
|
20
|
+
ECONNREFUSED: "the connection was refused, so no Postgres is listening there",
|
|
21
|
+
ENOTFOUND: "the host name does not resolve",
|
|
22
|
+
CONNECT_TIMEOUT: "the connection timed out",
|
|
23
|
+
ETIMEDOUT: "the connection timed out",
|
|
24
|
+
"28P01": "the password was rejected",
|
|
25
|
+
"28000": "the user was rejected",
|
|
26
|
+
"3D000": "the database does not exist"
|
|
27
|
+
};
|
|
28
|
+
const checkDatabase = async (url, projectDir) => {
|
|
29
|
+
const sql = connect(url, { connectTimeout: 3 });
|
|
30
|
+
const failure = await sql`select 1`.then(() => void 0, (error) => {
|
|
31
|
+
if (!error.code || !(error.code in reasons)) throw error;
|
|
32
|
+
return reasons[error.code];
|
|
33
|
+
});
|
|
34
|
+
await sql.end();
|
|
35
|
+
if (failure) throw new Error(`Cannot reach the database at ${describeTarget(url)}: ${failure}.${await startHint(projectDir)}`);
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { checkDatabase };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createPgDb } from "../project/pg-db.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/dev/create-db.ts
|
|
5
|
+
const sharedDb = (projectDir, url) => {
|
|
6
|
+
const store = globalThis;
|
|
7
|
+
if (store.__protobaseDb) return store.__protobaseDb;
|
|
8
|
+
const load = createRequire(path.join(projectDir, "package.json"));
|
|
9
|
+
store.__protobaseDb = createPgDb(load("pg"), url);
|
|
10
|
+
return store.__protobaseDb;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { sharedDb };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/dev/entry.d.ts
|
|
2
|
+
declare const _default: import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, import("hono/types").BlankSchema | import("hono/types").MergeSchemaPath<import("hono/types").BlankSchema, "/">, "/", "/">;
|
|
3
|
+
//#endregion
|
|
4
|
+
export { _default as default };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { loadProject, projectDb } from "./project.js";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { configExports, createAdmin, hasUsers } from "@protobase/server";
|
|
4
|
+
//#region src/dev/entry.ts
|
|
5
|
+
const projectDir = process.env.PROTOBASE_PROJECT;
|
|
6
|
+
const project = await loadProject(projectDir);
|
|
7
|
+
const db = projectDb(project, projectDir);
|
|
8
|
+
const { resources, views, pages, userMenu } = configExports(project.exports);
|
|
9
|
+
if (!project.authenticate) throw new Error("No authenticator: export `authenticate` (and `auth`) from protobase.config.ts; see https://docs.protobase.net/reference/auth/");
|
|
10
|
+
const admin = createAdmin({
|
|
11
|
+
resources,
|
|
12
|
+
views,
|
|
13
|
+
pages,
|
|
14
|
+
...userMenu && { userMenu },
|
|
15
|
+
db,
|
|
16
|
+
authenticate: project.authenticate,
|
|
17
|
+
...project.auth && { auth: project.auth },
|
|
18
|
+
options: {
|
|
19
|
+
onUnhandledError: (error) => console.error(error),
|
|
20
|
+
...project.options
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
var entry_default = new Hono().route("/", admin);
|
|
24
|
+
const store = globalThis;
|
|
25
|
+
if (project.auth && !store.__protobaseNoUsersShown) {
|
|
26
|
+
store.__protobaseNoUsersShown = true;
|
|
27
|
+
hasUsers(project.auth).then((any) => {
|
|
28
|
+
if (!any) console.log("No users yet. Create the first admin with: protobase users create you@example.com");
|
|
29
|
+
}, (error) => console.error(`Could not check for users, is the auth schema migrated? ${error.message}`));
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { entry_default as default };
|
package/dist/dev/env.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/dev/env.ts
|
|
4
|
+
const loadNearestEnvFile = (from) => {
|
|
5
|
+
for (let dir = from;; dir = path.dirname(dir)) {
|
|
6
|
+
const file = path.join(dir, ".env");
|
|
7
|
+
if (existsSync(file)) {
|
|
8
|
+
process.loadEnvFile(file);
|
|
9
|
+
return file;
|
|
10
|
+
}
|
|
11
|
+
if (path.dirname(dir) === dir) return void 0;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { loadNearestEnvFile };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { conventionConfig } from "../project/convention.js";
|
|
2
|
+
import { sharedDb } from "./create-db.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readdir } from "node:fs/promises";
|
|
6
|
+
//#region src/dev/project.ts
|
|
7
|
+
const load = (file) => import(
|
|
8
|
+
/* @vite-ignore */
|
|
9
|
+
`/@fs${file}`
|
|
10
|
+
);
|
|
11
|
+
const conventionExports = async (projectDir) => {
|
|
12
|
+
const configDir = path.join(projectDir, "config");
|
|
13
|
+
const index = path.join(configDir, "index.ts");
|
|
14
|
+
if (!existsSync(index)) throw new Error(`No config/index.ts in ${projectDir}; run \`protobase scaffold\` or add a protobase.config.ts`);
|
|
15
|
+
const indexExports = await load(index);
|
|
16
|
+
const dirs = (await readdir(configDir, { withFileTypes: true })).filter((entry) => entry.isDirectory());
|
|
17
|
+
const uiModules = [];
|
|
18
|
+
for (const { name } of dirs) {
|
|
19
|
+
const ui = path.join(configDir, name, "ui.ts");
|
|
20
|
+
if (existsSync(ui)) uiModules.push([name, await load(ui)]);
|
|
21
|
+
}
|
|
22
|
+
return conventionConfig(indexExports, uiModules);
|
|
23
|
+
};
|
|
24
|
+
const loadProject = async (projectDir) => {
|
|
25
|
+
const file = path.join(projectDir, "protobase.config.ts");
|
|
26
|
+
const custom = existsSync(file) ? (await load(file)).default ?? {} : {};
|
|
27
|
+
return {
|
|
28
|
+
...custom,
|
|
29
|
+
exports: custom.config ?? await conventionExports(projectDir)
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const projectDb = (project, projectDir) => {
|
|
33
|
+
if (project.db) return project.db;
|
|
34
|
+
const url = process.env.PROTOBASE_DATABASE_URL;
|
|
35
|
+
if (!url) throw new Error("No database: set DATABASE_URL (or pass --env) or export `db` from protobase.config.ts");
|
|
36
|
+
return sharedDb(projectDir, url);
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { loadProject, projectDb };
|
package/dist/dev/run.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { moduleFile } from "../module-file.js";
|
|
2
|
+
import { layoutCheck } from "../build/layout-check.js";
|
|
3
|
+
import { adminAppDir, adminAppMain, uiPackageDir } from "../project/admin-app.js";
|
|
4
|
+
import { projectUiPlugin } from "../project/project-ui.js";
|
|
5
|
+
import { loadNearestEnvFile } from "./env.js";
|
|
6
|
+
import { checkDatabase } from "./check-database.js";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
10
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
11
|
+
import react from "@vitejs/plugin-react";
|
|
12
|
+
import devServer from "@hono/vite-dev-server";
|
|
13
|
+
//#region src/dev/run.ts
|
|
14
|
+
const entry = moduleFile("./entry", import.meta.url);
|
|
15
|
+
const requireAppFiles = (appDir) => {
|
|
16
|
+
const missing = ["index.html", adminAppMain].filter((file) => !existsSync(path.join(appDir, file)));
|
|
17
|
+
if (missing.length > 0) throw new Error(`The admin app is missing ${missing.join(", ")} in ${appDir}; \`protobase dev\` serves the app from there`);
|
|
18
|
+
};
|
|
19
|
+
const reloadOnProjectChange = (projectDir) => ({
|
|
20
|
+
name: "protobase-project-reload",
|
|
21
|
+
handleHotUpdate: ({ file, server }) => {
|
|
22
|
+
if (file.startsWith(`${projectDir}${path.sep}`) && !file.includes(`${path.sep}node_modules${path.sep}`)) server.environments.ssr.moduleGraph.invalidateAll();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const appDependencies = [
|
|
26
|
+
"react",
|
|
27
|
+
"react/jsx-runtime",
|
|
28
|
+
"react/jsx-dev-runtime",
|
|
29
|
+
"react-dom",
|
|
30
|
+
"react-dom/client",
|
|
31
|
+
"lucide-react",
|
|
32
|
+
"zod",
|
|
33
|
+
"@tanstack/react-query",
|
|
34
|
+
"@tanstack/react-table",
|
|
35
|
+
"@tanstack/react-virtual",
|
|
36
|
+
"recharts"
|
|
37
|
+
];
|
|
38
|
+
const runDev = async (options, out) => {
|
|
39
|
+
loadNearestEnvFile(options.projectDir);
|
|
40
|
+
const url = process.env[options.envVar];
|
|
41
|
+
if (!url) throw new Error(`${options.envVar} is not set; add it to .env or pass --env <VARIABLE>`);
|
|
42
|
+
const appDir = options.appDir ?? adminAppDir;
|
|
43
|
+
requireAppFiles(appDir);
|
|
44
|
+
await checkDatabase(url, options.projectDir);
|
|
45
|
+
process.env.PROTOBASE_PROJECT = options.projectDir;
|
|
46
|
+
process.env.PROTOBASE_DATABASE_URL = url;
|
|
47
|
+
const server = await createServer({
|
|
48
|
+
root: appDir,
|
|
49
|
+
configFile: false,
|
|
50
|
+
...options.cacheDir && { cacheDir: options.cacheDir },
|
|
51
|
+
optimizeDeps: { include: appDependencies },
|
|
52
|
+
server: {
|
|
53
|
+
port: options.port,
|
|
54
|
+
strictPort: true,
|
|
55
|
+
...options.allowedHosts.length > 0 && { allowedHosts: options.allowedHosts },
|
|
56
|
+
fs: { allow: [
|
|
57
|
+
searchForWorkspaceRoot(options.projectDir),
|
|
58
|
+
uiPackageDir,
|
|
59
|
+
options.projectDir
|
|
60
|
+
] }
|
|
61
|
+
},
|
|
62
|
+
plugins: [
|
|
63
|
+
reloadOnProjectChange(options.projectDir),
|
|
64
|
+
projectUiPlugin(options.projectDir),
|
|
65
|
+
layoutCheck(),
|
|
66
|
+
react(),
|
|
67
|
+
tailwindcss(),
|
|
68
|
+
devServer({
|
|
69
|
+
entry,
|
|
70
|
+
exclude: [/^(?!\/api(\/|\?|$)).*/],
|
|
71
|
+
injectClientScript: false
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
await server.listen();
|
|
76
|
+
await server.ssrLoadModule(entry).catch(async (error) => {
|
|
77
|
+
await server.close();
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
const address = server.httpServer?.address();
|
|
81
|
+
const port = typeof address === "object" && address ? address.port : options.port;
|
|
82
|
+
out(`protobase dev ready at http://localhost:${port}\n`);
|
|
83
|
+
out(` project ${options.projectDir}\n api http://localhost:${port}/api/v1 (docs at /api/docs)\n`);
|
|
84
|
+
out(" login sign in with a user created by `protobase users create`\n");
|
|
85
|
+
return server;
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { runDev };
|