@protobase/cli 0.9.0 → 0.10.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/dev/run.js +1 -1
- package/dist/infer/names.js +6 -1
- package/dist/infer/proposal.js +2 -2
- package/dist/package.js +1 -1
- package/dist/project/project-admin.js +2 -1
- package/dist/scaffold/data-file.js +2 -1
- package/dist/scaffold/existing-config.js +5 -3
- package/dist/scaffold/plan.js +9 -7
- package/dist/scaffold/resource-names.js +4 -4
- package/dist/scaffold/ui-file.js +4 -4
- package/dist/serve/app.js +2 -2
- package/package.json +6 -6
package/dist/dev/run.js
CHANGED
package/dist/infer/names.js
CHANGED
|
@@ -3,6 +3,11 @@ const words = (name) => name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
|
3
3
|
const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
4
4
|
const lowerFirst = (word) => word.charAt(0).toLowerCase() + word.slice(1);
|
|
5
5
|
const identifier = (name) => /^[0-9]/.test(name) ? `c${name}` : name;
|
|
6
|
+
const snakeName = (name) => {
|
|
7
|
+
const parts = words(name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2"));
|
|
8
|
+
if (parts.length === 0) throw new Error(`Cannot derive a field name from "${name}"`);
|
|
9
|
+
return identifier(parts.map((part) => part.toLowerCase()).join("_"));
|
|
10
|
+
};
|
|
6
11
|
const camelCase = (name) => {
|
|
7
12
|
const parts = words(name);
|
|
8
13
|
if (parts.length === 0) throw new Error(`Cannot derive a field name from "${name}"`);
|
|
@@ -25,4 +30,4 @@ const humanizeSingular = (name) => {
|
|
|
25
30
|
return [...parts.slice(0, -1), singular(parts.at(-1).toLowerCase())].join(" ").replace(/^./, (c) => c.toUpperCase());
|
|
26
31
|
};
|
|
27
32
|
//#endregion
|
|
28
|
-
export { camelCase, humanize, humanizeSingular, singular };
|
|
33
|
+
export { camelCase, humanize, humanizeSingular, singular, snakeName };
|
package/dist/infer/proposal.js
CHANGED
|
@@ -3,7 +3,7 @@ import { checkEnumValues } from "./check-values.js";
|
|
|
3
3
|
import { columnDefault } from "./defaults.js";
|
|
4
4
|
import { inferKind } from "./type-map.js";
|
|
5
5
|
import { defaultColumn } from "./default-column.js";
|
|
6
|
-
import {
|
|
6
|
+
import { snakeName } from "./names.js";
|
|
7
7
|
import { isTenantColumn } from "./tenant.js";
|
|
8
8
|
//#region src/infer/proposal.ts
|
|
9
9
|
const columnIndexing = (table, column) => {
|
|
@@ -41,7 +41,7 @@ const defaultsOf = (table, column, kind) => {
|
|
|
41
41
|
const proposeField = (table, column, context) => {
|
|
42
42
|
const kind = inferKind(column, checkEnumValues(table.checks, column.name));
|
|
43
43
|
const relation = relationOf(table, column.name, context);
|
|
44
|
-
const name =
|
|
44
|
+
const name = snakeName(column.name);
|
|
45
45
|
const defaults = defaultsOf(table, column, kind);
|
|
46
46
|
return {
|
|
47
47
|
name,
|
package/dist/package.js
CHANGED
|
@@ -5,13 +5,14 @@ import { configExports, createAdmin, jobDefinitions } from "@protobase/server";
|
|
|
5
5
|
* pages and subscriptions, and the rest of its config. `jobs` are the project's own and the delivery of its events.
|
|
6
6
|
*/
|
|
7
7
|
const projectAdmin = (project, config, db) => {
|
|
8
|
-
const { resources, views, pages, userMenu, subscriptions } = configExports(config);
|
|
8
|
+
const { resources, views, pages, userMenu, bottomBars, subscriptions } = configExports(config);
|
|
9
9
|
const own = jobDefinitions(project.jobs ?? {});
|
|
10
10
|
const admin = createAdmin({
|
|
11
11
|
resources,
|
|
12
12
|
views,
|
|
13
13
|
pages,
|
|
14
14
|
...userMenu && { userMenu },
|
|
15
|
+
bottomBars,
|
|
15
16
|
db,
|
|
16
17
|
authenticate: project.authenticate,
|
|
17
18
|
...project.auth && { auth: project.auth },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { tableKey } from "../db/model.js";
|
|
2
|
+
import { camelCase } from "../infer/names.js";
|
|
2
3
|
import { parseSource } from "./project.js";
|
|
3
4
|
import { fieldsObject, resourceChain } from "./existing-config.js";
|
|
4
5
|
import { renderField } from "../infer/render-field.js";
|
|
@@ -19,7 +20,7 @@ const renderDataFile = (plan) => {
|
|
|
19
20
|
return [
|
|
20
21
|
`import { f, resource } from '@protobase/schema'`,
|
|
21
22
|
"",
|
|
22
|
-
`${docComment(plan)}export const ${plan.name} = resource('${plan.name}')`,
|
|
23
|
+
`${docComment(plan)}export const ${camelCase(plan.name)} = resource('${plan.name}')`,
|
|
23
24
|
` .table('${tableKey(plan.table)}')`,
|
|
24
25
|
` .fields({`,
|
|
25
26
|
properties,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultColumn } from "../infer/default-column.js";
|
|
2
|
-
import { camelCase } from "../infer/names.js";
|
|
2
|
+
import { camelCase, snakeName } from "../infer/names.js";
|
|
3
3
|
import { parseSource } from "./project.js";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { readFile, readdir } from "node:fs/promises";
|
|
@@ -65,9 +65,11 @@ const parseResource = (text, dir) => {
|
|
|
65
65
|
const tableCall = methodCalls(chainTop, "table")[0];
|
|
66
66
|
const table = tableCall && stringArg(tableCall);
|
|
67
67
|
const fields = fieldsObject(chainTop);
|
|
68
|
-
|
|
68
|
+
const identifier = root.getFirstAncestorByKind(SyntaxKind.VariableDeclaration)?.getName();
|
|
69
|
+
if (!name || !table || !fields || !identifier) return void 0;
|
|
69
70
|
return {
|
|
70
71
|
name,
|
|
72
|
+
identifier,
|
|
71
73
|
table,
|
|
72
74
|
dir,
|
|
73
75
|
text,
|
|
@@ -77,7 +79,7 @@ const parseResource = (text, dir) => {
|
|
|
77
79
|
})
|
|
78
80
|
};
|
|
79
81
|
};
|
|
80
|
-
const findField = (resource, column) => resource.fields.find((field) => field.ignored ? field.column.toLowerCase() === column.toLowerCase() || field.name === camelCase(column) : field.column === column);
|
|
82
|
+
const findField = (resource, column) => resource.fields.find((field) => field.ignored ? field.column.toLowerCase() === column.toLowerCase() || field.name === snakeName(column) || field.name === camelCase(column) : field.column === column);
|
|
81
83
|
const readExistingResources = async (configDir) => {
|
|
82
84
|
const entries = await readdir(configDir, { withFileTypes: true }).catch((error) => {
|
|
83
85
|
if (error.code === "ENOENT") return [];
|
package/dist/scaffold/plan.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tableKey } from "../db/model.js";
|
|
2
2
|
import { keyColumns } from "../db/keys.js";
|
|
3
|
+
import { camelCase } from "../infer/names.js";
|
|
3
4
|
import { findField } from "./existing-config.js";
|
|
4
5
|
import { proposeField } from "../infer/proposal.js";
|
|
5
6
|
import { addFieldsToDataFile, renderDataFile } from "./data-file.js";
|
|
@@ -49,19 +50,20 @@ const planScaffold = async (input) => {
|
|
|
49
50
|
const drift = [];
|
|
50
51
|
const warnings = [];
|
|
51
52
|
const registrations = [];
|
|
52
|
-
const writeUi = async (dir, resource, table, current, decisions) => {
|
|
53
|
+
const writeUi = async (dir, resource, identifier, table, current, decisions) => {
|
|
53
54
|
if (ui.length === 0) return;
|
|
54
55
|
const file = path.join(configDir, dir, "ui.ts");
|
|
55
56
|
const before = await input.readText(file);
|
|
56
57
|
const target = {
|
|
57
58
|
resource,
|
|
59
|
+
identifier,
|
|
58
60
|
tableName: table.name,
|
|
59
61
|
fields: uiFields(table, current, decisions)
|
|
60
62
|
};
|
|
61
63
|
changes.push(...change(file, before, before === void 0 ? renderUiFile(target, ui) : addUiSections(before, target, ui)));
|
|
62
64
|
if (before === void 0) registrations.push({
|
|
63
65
|
from: `./${dir}/ui`,
|
|
64
|
-
names: [viewName(
|
|
66
|
+
names: [viewName(identifier)]
|
|
65
67
|
});
|
|
66
68
|
};
|
|
67
69
|
for (const table of targets) {
|
|
@@ -75,7 +77,7 @@ const planScaffold = async (input) => {
|
|
|
75
77
|
const file = path.join(configDir, current.dir, "data.ts");
|
|
76
78
|
changes.push(...change(file, current.text, addFieldsToDataFile(current.text, decisions)));
|
|
77
79
|
drift.push(...detectDrift(current, table, context(table)));
|
|
78
|
-
await writeUi(current.dir, current.name, table, current, decisions);
|
|
80
|
+
await writeUi(current.dir, current.name, current.identifier, table, current, decisions);
|
|
79
81
|
continue;
|
|
80
82
|
}
|
|
81
83
|
const name = names.get(key);
|
|
@@ -93,9 +95,9 @@ const planScaffold = async (input) => {
|
|
|
93
95
|
});
|
|
94
96
|
registrations.push({
|
|
95
97
|
from: `./${name}/data`,
|
|
96
|
-
names: [name]
|
|
98
|
+
names: [camelCase(name)]
|
|
97
99
|
});
|
|
98
|
-
await writeUi(name, name, table, void 0, decisions);
|
|
100
|
+
await writeUi(name, name, camelCase(name), table, void 0, decisions);
|
|
99
101
|
}
|
|
100
102
|
const indexFile = path.join(configDir, "index.ts");
|
|
101
103
|
const indexBefore = await input.readText(indexFile);
|
|
@@ -114,12 +116,12 @@ const registerExisting = async (input, planned) => {
|
|
|
114
116
|
const data = `./${resource.dir}/data`;
|
|
115
117
|
if (!known.has(data)) registrations.push({
|
|
116
118
|
from: data,
|
|
117
|
-
names: [resource.
|
|
119
|
+
names: [resource.identifier]
|
|
118
120
|
});
|
|
119
121
|
const ui = `./${resource.dir}/ui`;
|
|
120
122
|
if (await input.readText(path.join(input.configDir, resource.dir, "ui.ts")) !== void 0 && !known.has(ui)) registrations.push({
|
|
121
123
|
from: ui,
|
|
122
|
-
names: [viewName(resource.
|
|
124
|
+
names: [viewName(resource.identifier)]
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
return registrations;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { tableKey } from "../db/model.js";
|
|
2
|
-
import {
|
|
2
|
+
import { snakeName } from "../infer/names.js";
|
|
3
3
|
//#region src/scaffold/resource-names.ts
|
|
4
4
|
const assignNames = (tables, existing) => {
|
|
5
5
|
const names = new Map(existing.map((resource) => [resource.table, resource.name]));
|
|
6
6
|
const taken = new Set(names.values());
|
|
7
7
|
const fresh = tables.filter((table) => !names.has(tableKey(table)));
|
|
8
8
|
const counts = /* @__PURE__ */ new Map();
|
|
9
|
-
for (const table of tables) counts.set(
|
|
9
|
+
for (const table of tables) counts.set(snakeName(table.name), (counts.get(snakeName(table.name)) ?? 0) + 1);
|
|
10
10
|
for (const table of fresh) {
|
|
11
|
-
const plain =
|
|
12
|
-
const name = counts.get(plain) === 1 && !taken.has(plain) ? plain :
|
|
11
|
+
const plain = snakeName(table.name);
|
|
12
|
+
const name = counts.get(plain) === 1 && !taken.has(plain) ? plain : snakeName(`${table.schema}_${table.name}`);
|
|
13
13
|
names.set(tableKey(table), name);
|
|
14
14
|
taken.add(name);
|
|
15
15
|
}
|
package/dist/scaffold/ui-file.js
CHANGED
|
@@ -3,7 +3,7 @@ import { parseSource } from "./project.js";
|
|
|
3
3
|
import { methodCalls } from "./existing-config.js";
|
|
4
4
|
import { Node, SyntaxKind } from "ts-morph";
|
|
5
5
|
//#region src/scaffold/ui-file.ts
|
|
6
|
-
const auditField = /^(created|updated)(At|On)$/;
|
|
6
|
+
const auditField = /^(created|updated)(At|On|_at|_on)$/;
|
|
7
7
|
const longText = /^(notes?|description|body|comment|comments)$/;
|
|
8
8
|
const listSize = 5;
|
|
9
9
|
const listColumns = (fields) => {
|
|
@@ -13,7 +13,7 @@ const listColumns = (fields) => {
|
|
|
13
13
|
const picked = [...scalars.slice(0, listSize), ...scalars.length < 3 ? relations : []].slice(0, listSize);
|
|
14
14
|
return (picked.length > 0 ? picked : fields.filter((f) => f.role === "key")).map((f) => f.name);
|
|
15
15
|
};
|
|
16
|
-
const viewName = (
|
|
16
|
+
const viewName = (identifier) => `${identifier}View`;
|
|
17
17
|
const sectionText = {
|
|
18
18
|
names: (t) => `.names({ singular: '${humanizeSingular(t.tableName)}', plural: '${humanize(t.tableName)}' })`,
|
|
19
19
|
list: (t) => `.list((r) => ({ columns: [${listColumns(t.fields).map((n) => `r.${n}`).join(", ")}] }))`
|
|
@@ -21,9 +21,9 @@ const sectionText = {
|
|
|
21
21
|
const lines = (sections, target) => sections.map((s) => ` ${sectionText[s](target)}`);
|
|
22
22
|
const renderUiFile = (target, sections) => [
|
|
23
23
|
`import { view } from '@protobase/schema'`,
|
|
24
|
-
`import type { ${target.
|
|
24
|
+
`import type { ${target.identifier} } from './data'`,
|
|
25
25
|
"",
|
|
26
|
-
`export const ${viewName(target.
|
|
26
|
+
`export const ${viewName(target.identifier)} = view<typeof ${target.identifier}>('${target.resource}')`,
|
|
27
27
|
...lines(sections, target),
|
|
28
28
|
""
|
|
29
29
|
].join("\n");
|
package/dist/serve/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isApiPath } from "../bundle/manifest.js";
|
|
2
2
|
import { serveStaticSite } from "./static-site.js";
|
|
3
|
-
import { jobsDrainApp } from "@protobase/server";
|
|
3
|
+
import { jobsDrainApp, webauthnPath } from "@protobase/server";
|
|
4
4
|
import { Hono } from "hono";
|
|
5
5
|
import { logger } from "hono/logger";
|
|
6
6
|
//#region src/serve/app.ts
|
|
@@ -8,7 +8,7 @@ const createServeApp = ({ admin, requestLog, site, jobs, drainToken }) => {
|
|
|
8
8
|
const app = new Hono();
|
|
9
9
|
if (requestLog) app.use(logger());
|
|
10
10
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
11
|
-
if (site) app.use("*", (c, next) => isApiPath(site.api, c.req.path) ? next() : serveStaticSite(site, c.req.raw));
|
|
11
|
+
if (site) app.use("*", (c, next) => isApiPath(site.api, c.req.path) || c.req.path === webauthnPath ? next() : serveStaticSite(site, c.req.raw));
|
|
12
12
|
if (jobs && drainToken) app.route("/", jobsDrainApp({
|
|
13
13
|
runtime: jobs,
|
|
14
14
|
token: drainToken
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@hono/node-server": "^2.1.3",
|
|
36
36
|
"@hono/vite-dev-server": "^0.26.1",
|
|
37
|
-
"@protobase/layout": "0.
|
|
38
|
-
"@protobase/query": "0.
|
|
39
|
-
"@protobase/schema": "0.
|
|
40
|
-
"@protobase/server": "0.
|
|
41
|
-
"@protobase/ui": "0.
|
|
37
|
+
"@protobase/layout": "0.10.0",
|
|
38
|
+
"@protobase/query": "0.10.0",
|
|
39
|
+
"@protobase/schema": "0.10.0",
|
|
40
|
+
"@protobase/server": "0.10.0",
|
|
41
|
+
"@protobase/ui": "0.10.0",
|
|
42
42
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
43
43
|
"@tailwindcss/vite": "^4.3.3",
|
|
44
44
|
"@vitejs/plugin-react": "^6.1.2",
|