ascua-scaffold 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/generate.js +124 -0
- package/index.js +179 -88
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,31 @@ un `<form>` de HTML, así que también funciona sin JavaScript.
|
|
|
83
83
|
- Un `UNIQUE` repetido vuelve con un 409 y el error bajo la celda de esa
|
|
84
84
|
fila.
|
|
85
85
|
|
|
86
|
+
## El admin
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
stil exec ascua-scaffold --admin
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Genera todas las tablas a la vez bajo `/admin`, como el admin de Django:
|
|
93
|
+
lista, alta, edición y grilla de cada una, un panel en `/admin` y una barra
|
|
94
|
+
con las tablas.
|
|
95
|
+
|
|
96
|
+
- **Pide usuario y contraseña** (HTTP Basic): `ADMIN_PASSWORD` y
|
|
97
|
+
`ADMIN_USER` («admin» por defecto). **Sin `ADMIN_PASSWORD`, `/admin` es un
|
|
98
|
+
404**: nunca queda abierto por olvido. Va en `src/routes/admin/_middleware.ts`,
|
|
99
|
+
para cambiarlo por el inicio de sesión del sitio (passkeys, una sesión)
|
|
100
|
+
cuando lo tenga.
|
|
101
|
+
- **Reutiliza lo que hay**: una tabla que ya tiene sus consultas y su
|
|
102
|
+
`src/resources/<tabla>.ts` —de un `ascua-scaffold <tabla>` anterior— los
|
|
103
|
+
conserva y solo recibe sus páginas del admin.
|
|
104
|
+
- **Lo que no puede generar lo salta y dice por qué**: vistas, claves
|
|
105
|
+
compuestas, columnas obligatorias que el formulario no sabe editar. El
|
|
106
|
+
panel las lista.
|
|
107
|
+
- **Se puede volver a correr**: tras una migración nueva, agrega las tablas
|
|
108
|
+
nuevas y deja como están las que ya tenían página. `--force` las reescribe
|
|
109
|
+
todas.
|
|
110
|
+
|
|
86
111
|
## Requisitos
|
|
87
112
|
|
|
88
113
|
- Un sitio con servidor: `ascua({ site: { mode: "server" } })` en
|
|
@@ -97,6 +122,7 @@ un `<form>` de HTML, así que también funciona sin JavaScript.
|
|
|
97
122
|
|
|
98
123
|
```text
|
|
99
124
|
ascua-scaffold <tabla> [--config brasa.json] [--force]
|
|
125
|
+
ascua-scaffold --admin [--config brasa.json] [--force]
|
|
100
126
|
```
|
|
101
127
|
|
|
102
128
|
## Licencia
|
package/generate.js
CHANGED
|
@@ -765,6 +765,130 @@ export default function ${n.plural}Grid({ data, form }: { data: Awaited<ReturnTy
|
|
|
765
765
|
`;
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
+
/** `src/resources/admin.ts`: las tablas del admin. Se reescribe en cada `--admin`. */
|
|
769
|
+
export function datosAdmin(incluidas, saltadas) {
|
|
770
|
+
const filas = incluidas.map((x) => ` { name: ${JSON.stringify(x.name)}, label: ${JSON.stringify(etiqueta(x.name))}, path: ${JSON.stringify(x.path)} },`).join("\n");
|
|
771
|
+
// Los motivos vienen de la CLI, con \`nombres\` entre comillas invertidas: en la página, sin ellas.
|
|
772
|
+
const fuera = saltadas.map((x) => ` { name: ${JSON.stringify(x.name)}, reason: ${JSON.stringify(x.reason.replace(/\`/g, ""))} },`).join("\n");
|
|
773
|
+
return `// Written by \`ascua-scaffold --admin\` each time it runs: edits here are lost.
|
|
774
|
+
|
|
775
|
+
/** The tables the admin manages, in the order of the migrations. */
|
|
776
|
+
export const adminTables: readonly { name: string; label: string; path: string }[] = [
|
|
777
|
+
${filas}
|
|
778
|
+
];
|
|
779
|
+
|
|
780
|
+
/** The tables it left out, and why. */
|
|
781
|
+
export const skippedTables: readonly { name: string; reason: string }[] = [
|
|
782
|
+
${fuera}
|
|
783
|
+
];
|
|
784
|
+
`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** `src/routes/admin/_middleware.ts`: sin contraseña configurada, el admin no existe. */
|
|
788
|
+
export function guardiaAdmin() {
|
|
789
|
+
return `// Generated by ascua-scaffold. Yours to edit: it is not generated again.
|
|
790
|
+
import { notFound, type Middleware } from "vite-plugin-ascua/site";
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* The admin asks for a user and a password (HTTP Basic): ADMIN_USER, "admin"
|
|
794
|
+
* by default, and ADMIN_PASSWORD. Without ADMIN_PASSWORD the admin does not
|
|
795
|
+
* exist: every page under /admin is a 404. Serve it over HTTPS only, and swap
|
|
796
|
+
* this for the site's own sign-in (passkeys, a session) when it has one.
|
|
797
|
+
*/
|
|
798
|
+
const middleware: Middleware = ({ request, setHeaders }) => {
|
|
799
|
+
setHeaders({ "cache-control": "no-store", "x-robots-tag": "noindex" });
|
|
800
|
+
const password = process.env.ADMIN_PASSWORD;
|
|
801
|
+
if (!password) throw notFound();
|
|
802
|
+
const expected = \`Basic \${base64(\`\${process.env.ADMIN_USER || "admin"}:\${password}\`)}\`;
|
|
803
|
+
if (!same(request.headers.get("authorization") ?? "", expected)) {
|
|
804
|
+
return new Response("Sign in to the admin.", {
|
|
805
|
+
status: 401,
|
|
806
|
+
headers: { "www-authenticate": 'Basic realm="admin", charset="UTF-8"', "cache-control": "no-store" },
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
export default middleware;
|
|
812
|
+
|
|
813
|
+
/** UTF-8 to base64, the same on Node and on Workers. */
|
|
814
|
+
function base64(text: string): string {
|
|
815
|
+
return btoa(String.fromCharCode(...new TextEncoder().encode(text)));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Compares without stopping at the first difference: the time it takes does not tell how much matched. */
|
|
819
|
+
function same(a: string, b: string): boolean {
|
|
820
|
+
let diff = a.length ^ b.length;
|
|
821
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
822
|
+
return diff === 0;
|
|
823
|
+
}
|
|
824
|
+
`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** `src/routes/admin/_layout.ts`: la barra con las tablas. */
|
|
828
|
+
export function marcoAdmin(datos) {
|
|
829
|
+
return `// Generated by ascua-scaffold. Yours to edit: it is not generated again.
|
|
830
|
+
import type { LayoutProps } from "vite-plugin-ascua/site";
|
|
831
|
+
|
|
832
|
+
import { adminTables } from "${datos}";
|
|
833
|
+
|
|
834
|
+
type AdminTable = (typeof adminTables)[number];
|
|
835
|
+
|
|
836
|
+
/** Every admin page: the tables on the side, the page on the right. */
|
|
837
|
+
export default function AdminLayout(props: LayoutProps) {
|
|
838
|
+
const frame = view\`
|
|
839
|
+
<div class="admin">
|
|
840
|
+
<nav>
|
|
841
|
+
<a href="/admin"><strong>Admin</strong></a>
|
|
842
|
+
<For each=\${() => adminTables} key=\${(t: AdminTable) => t.name}
|
|
843
|
+
render=\${(t: AdminTable) => view\`<a href=\${t.path} class:current=\${props.path.startsWith(t.path)}>\${t.label}</a>\`}/>
|
|
844
|
+
</nav>
|
|
845
|
+
<div class="page"></div>
|
|
846
|
+
<style>
|
|
847
|
+
.admin { display: grid; grid-template-columns: 12rem 1fr; gap: 1.5rem; }
|
|
848
|
+
.admin nav { display: grid; align-content: start; gap: .25rem; }
|
|
849
|
+
.admin nav .current { font-weight: bold; }
|
|
850
|
+
@media (max-width: 40rem) { .admin { grid-template-columns: 1fr; } }
|
|
851
|
+
</style>
|
|
852
|
+
</div>\`;
|
|
853
|
+
props.children(frame.querySelector(".page")!);
|
|
854
|
+
return frame;
|
|
855
|
+
}
|
|
856
|
+
`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** `src/routes/admin/index.ts`: el panel. */
|
|
860
|
+
export function panelAdmin(datos) {
|
|
861
|
+
return `// Generated by ascua-scaffold. Yours to edit: it is not generated again.
|
|
862
|
+
import { adminTables, skippedTables } from "${datos}";
|
|
863
|
+
|
|
864
|
+
export const title = "Admin";
|
|
865
|
+
|
|
866
|
+
type AdminTable = (typeof adminTables)[number];
|
|
867
|
+
type Skipped = (typeof skippedTables)[number];
|
|
868
|
+
|
|
869
|
+
export default function Admin() {
|
|
870
|
+
return view\`
|
|
871
|
+
<section>
|
|
872
|
+
<h1>Admin</h1>
|
|
873
|
+
<table>
|
|
874
|
+
<thead><tr><th>Table</th><th></th></tr></thead>
|
|
875
|
+
<tbody>
|
|
876
|
+
<For each=\${() => adminTables} key=\${(t: AdminTable) => t.name}
|
|
877
|
+
render=\${(t: AdminTable) => view\`<tr><td><a href=\${t.path}>\${t.label}</a></td><td><a href=\${\`\${t.path}/new\`}>New</a> · <a href=\${\`\${t.path}/grid\`}>Grid</a></td></tr>\`}/>
|
|
878
|
+
</tbody>
|
|
879
|
+
</table>
|
|
880
|
+
<Show when=\${() => skippedTables.length > 0}>
|
|
881
|
+
<h2>Left out</h2>
|
|
882
|
+
<ul>
|
|
883
|
+
<For each=\${() => skippedTables} key=\${(t: Skipped) => t.name}
|
|
884
|
+
render=\${(t: Skipped) => view\`<li><code>\${t.name}</code>: \${t.reason}</li>\`}/>
|
|
885
|
+
</ul>
|
|
886
|
+
</Show>
|
|
887
|
+
</section>\`;
|
|
888
|
+
}
|
|
889
|
+
`;
|
|
890
|
+
}
|
|
891
|
+
|
|
768
892
|
/** `src/pool.ts`: un pool para todo el servidor, con las migraciones aplicadas una vez. */
|
|
769
893
|
export function piscina({ mysql, migrate, db }) {
|
|
770
894
|
if (mysql) {
|
package/index.js
CHANGED
|
@@ -16,10 +16,11 @@ import { createRequire } from "node:module";
|
|
|
16
16
|
import { dirname, join, relative, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
|
|
19
|
-
import { analizar, ayudaRejilla, consultasSql, nombres, piscina, recurso, rejilla, rutas, ScaffoldError, t } from "./generate.js";
|
|
19
|
+
import { analizar, ayudaRejilla, consultasSql, datosAdmin, guardiaAdmin, marcoAdmin, nombres, panelAdmin, piscina, recurso, rejilla, rutas, ScaffoldError, t } from "./generate.js";
|
|
20
20
|
|
|
21
21
|
const AYUDA = t(
|
|
22
22
|
`Uso: ascua-scaffold <tabla> [opciones]
|
|
23
|
+
ascua-scaffold --admin [opciones]
|
|
23
24
|
|
|
24
25
|
Genera, a partir de una tabla de las migraciones de brasa:
|
|
25
26
|
db/queries/<tabla>.sql List, Get, Create, Update y Delete
|
|
@@ -33,9 +34,11 @@ Genera, a partir de una tabla de las migraciones de brasa:
|
|
|
33
34
|
|
|
34
35
|
Opciones:
|
|
35
36
|
--config <ruta> el brasa.json (por defecto, el del directorio actual)
|
|
37
|
+
--admin todas las tablas bajo /admin, con su panel; pide ADMIN_PASSWORD
|
|
36
38
|
--force reescribe los archivos que ya existan
|
|
37
39
|
-h, --help esta ayuda`,
|
|
38
40
|
`Usage: ascua-scaffold <table> [options]
|
|
41
|
+
ascua-scaffold --admin [options]
|
|
39
42
|
|
|
40
43
|
Generates, from a table in the brasa migrations:
|
|
41
44
|
db/queries/<table>.sql List, Get, Create, Update and Delete
|
|
@@ -49,6 +52,7 @@ Generates, from a table in the brasa migrations:
|
|
|
49
52
|
|
|
50
53
|
Options:
|
|
51
54
|
--config <path> the brasa.json (default: the one in the current directory)
|
|
55
|
+
--admin every table under /admin, with a dashboard; asks for ADMIN_PASSWORD
|
|
52
56
|
--force overwrite files that already exist
|
|
53
57
|
-h, --help this help`,
|
|
54
58
|
);
|
|
@@ -57,11 +61,12 @@ const POR_DEFECTO = { dialect: "postgres", migrations: "db/migrations", queries:
|
|
|
57
61
|
|
|
58
62
|
/** `["posts", "--force"]` → `{ table: "posts", force: true }`. */
|
|
59
63
|
export function leerArgumentos(argv) {
|
|
60
|
-
const salida = { table: undefined, config: "brasa.json", force: false, ayuda: false };
|
|
64
|
+
const salida = { table: undefined, config: "brasa.json", force: false, admin: false, ayuda: false };
|
|
61
65
|
for (let i = 0; i < argv.length; i++) {
|
|
62
66
|
const a = argv[i];
|
|
63
67
|
if (a === "-h" || a === "--help") salida.ayuda = true;
|
|
64
68
|
else if (a === "--force") salida.force = true;
|
|
69
|
+
else if (a === "--admin") salida.admin = true;
|
|
65
70
|
else if (a === "--config") salida.config = argv[++i];
|
|
66
71
|
else if (a.startsWith("--config=")) salida.config = a.slice("--config=".length);
|
|
67
72
|
else if (!a.startsWith("-") && salida.table === undefined) salida.table = a;
|
|
@@ -99,140 +104,226 @@ function importar(desde, hacia) {
|
|
|
99
104
|
return r;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (args.ayuda || !args.table) {
|
|
105
|
-
console.log(AYUDA);
|
|
106
|
-
return args.ayuda ? 0 : 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
107
|
+
/** brasa.json, el esquema y el brasa del proyecto. */
|
|
108
|
+
async function proyecto(args) {
|
|
109
109
|
const rutaConfig = resolve(args.config);
|
|
110
110
|
const raiz = dirname(rutaConfig);
|
|
111
111
|
if (!existsSync(rutaConfig)) {
|
|
112
112
|
throw new ScaffoldError(t(`no existe ${rutaConfig}: crea uno con \`brasa init\``, `${rutaConfig} does not exist: create one with \`brasa init\``));
|
|
113
113
|
}
|
|
114
114
|
const config = { ...POR_DEFECTO, ...JSON.parse(readFileSync(rutaConfig, "utf8")) };
|
|
115
|
-
const mysql = config.dialect === "mysql";
|
|
116
|
-
const dirMigraciones = resolve(raiz, config.migrations);
|
|
117
|
-
const dirConsultas = resolve(raiz, config.queries);
|
|
118
|
-
const out = resolve(raiz, config.out);
|
|
119
|
-
|
|
120
115
|
const brasa = await cargarBrasa(raiz);
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
116
|
+
const dirConsultas = resolve(raiz, config.queries);
|
|
117
|
+
const salida = brasa.compile({
|
|
118
|
+
dialect: config.dialect,
|
|
119
|
+
migrate: config.migrate,
|
|
120
|
+
migrations: leerSql(resolve(raiz, config.migrations)),
|
|
121
|
+
queries: leerSql(dirConsultas),
|
|
122
|
+
});
|
|
124
123
|
if (salida.code === null) {
|
|
125
124
|
throw new ScaffoldError(t("el esquema o las consultas tienen errores: corre `brasa generate` para verlos", "the schema or the queries have errors: run `brasa generate` to see them"));
|
|
126
125
|
}
|
|
127
126
|
if (!salida.tables) {
|
|
128
127
|
throw new ScaffoldError(t("este brasa-sql no describe las tablas: actualízalo", "this brasa-sql does not describe tables: update it"));
|
|
129
128
|
}
|
|
129
|
+
return {
|
|
130
|
+
rutaConfig,
|
|
131
|
+
raiz,
|
|
132
|
+
config,
|
|
133
|
+
brasa,
|
|
134
|
+
salida,
|
|
135
|
+
dirConsultas,
|
|
136
|
+
mysql: config.dialect === "mysql",
|
|
137
|
+
out: resolve(raiz, config.out),
|
|
138
|
+
pool: join(raiz, "src", "pool.ts"),
|
|
139
|
+
ayuda: join(raiz, "src", "resources", "grid.ts"),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
130
142
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const a = analizar(tabla, { mysql });
|
|
143
|
+
/**
|
|
144
|
+
* Los archivos de una tabla y lo que llevan. Lanza `ScaffoldError` si no se
|
|
145
|
+
* puede generar. Con `reutilizar`, una tabla que ya tiene sus cinco consultas
|
|
146
|
+
* y su `resources/<tabla>.ts` los conserva y solo recibe rutas nuevas.
|
|
147
|
+
*/
|
|
148
|
+
function planTabla(p, tabla, { prefijo, force, reutilizar }) {
|
|
149
|
+
const a = analizar(tabla, { mysql: p.mysql });
|
|
140
150
|
const n = nombres(tabla);
|
|
141
|
-
// Con --force, las del archivo que se va a reescribir no cuentan.
|
|
142
|
-
const propio = join(dirConsultas, `${tabla.name}.sql`);
|
|
143
|
-
const ajenas = salida.queries.filter((x) => !(args.force && resolve(x.file) === propio));
|
|
144
|
-
const repetidas = [n.list, n.get, n.create, n.update, n.delete].filter((q) => ajenas.some((x) => x.name === q));
|
|
145
|
-
const enPropio = repetidas.filter((q) => salida.queries.some((x) => x.name === q && resolve(x.file) === propio));
|
|
146
|
-
if (repetidas.length && enPropio.length === repetidas.length) {
|
|
147
|
-
// Las del ejemplo de `brasa init`, por ejemplo: están en el archivo que se va a escribir.
|
|
148
|
-
const archivo = relative(process.cwd(), propio);
|
|
149
|
-
throw new ScaffoldError(
|
|
150
|
-
t(`${archivo} ya tiene ${repetidas.join(", ")}. Con --force se reemplaza ese archivo`, `${archivo} already has ${repetidas.join(", ")}. Use --force to replace that file`),
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (repetidas.length) {
|
|
154
|
-
throw new ScaffoldError(
|
|
155
|
-
t(`ya hay consultas con estos nombres: ${repetidas.join(", ")}. Quita esas o genera otra tabla`, `queries with these names already exist: ${repetidas.join(", ")}. Remove them or scaffold another table`),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
151
|
const carpeta = tabla.name.replace(/_/g, "-").toLowerCase();
|
|
160
152
|
if (carpeta === "grid") {
|
|
161
153
|
throw new ScaffoldError(t("una tabla `grid` chocaría con src/resources/grid.ts, que comparten las grillas", "a `grid` table would clash with src/resources/grid.ts, which the grids share"));
|
|
162
154
|
}
|
|
163
155
|
// Los enums de Postgres tienen su `<x>Values` en el módulo; los de MySQL
|
|
164
156
|
// (`ENUM('a','b')` en la columna), no: sus valores van escritos.
|
|
165
|
-
const enums = new Set(mysql ? [] : tabla.columns.filter((c) => c.kind === "enum").map((c) => c.sqlType));
|
|
157
|
+
const enums = new Set(p.mysql ? [] : tabla.columns.filter((c) => c.kind === "enum").map((c) => c.sqlType));
|
|
166
158
|
|
|
159
|
+
const sql = join(p.dirConsultas, `${tabla.name}.sql`);
|
|
160
|
+
const archivoRecurso = join(p.raiz, "src", "resources", `${carpeta}.ts`);
|
|
161
|
+
const consultas = [n.list, n.get, n.create, n.update, n.delete];
|
|
162
|
+
const hay = consultas.filter((q) => p.salida.queries.some((x) => x.name === q));
|
|
163
|
+
const reusa = reutilizar && !force && hay.length === consultas.length && existsSync(archivoRecurso);
|
|
164
|
+
|
|
165
|
+
if (!reusa) {
|
|
166
|
+
// Con --force, las del archivo que se va a reescribir no cuentan.
|
|
167
|
+
const ajenas = p.salida.queries.filter((x) => !(force && resolve(x.file) === sql));
|
|
168
|
+
const repetidas = consultas.filter((q) => ajenas.some((x) => x.name === q));
|
|
169
|
+
const enPropio = repetidas.filter((q) => p.salida.queries.some((x) => x.name === q && resolve(x.file) === sql));
|
|
170
|
+
if (repetidas.length && enPropio.length === repetidas.length) {
|
|
171
|
+
// Las del ejemplo de `brasa init`, por ejemplo: están en el archivo que se va a escribir.
|
|
172
|
+
const archivo = relative(process.cwd(), sql);
|
|
173
|
+
throw new ScaffoldError(t(`${archivo} ya tiene ${repetidas.join(", ")}. Con --force se reemplaza ese archivo`, `${archivo} already has ${repetidas.join(", ")}. Use --force to replace that file`));
|
|
174
|
+
}
|
|
175
|
+
if (repetidas.length) {
|
|
176
|
+
throw new ScaffoldError(t(`ya hay consultas con estos nombres: ${repetidas.join(", ")}. Quita esas o genera otra tabla`, `queries with these names already exist: ${repetidas.join(", ")}. Remove them or scaffold another table`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dirRutas = join(p.raiz, "src", "routes", ...(prefijo ? [prefijo] : []), carpeta);
|
|
181
|
+
const base = `${prefijo ? `/${prefijo}` : ""}/${carpeta}`;
|
|
167
182
|
const archivos = {
|
|
168
|
-
sql
|
|
169
|
-
recurso:
|
|
170
|
-
index: join(
|
|
171
|
-
nuevo: join(
|
|
172
|
-
editar: join(
|
|
173
|
-
rejilla: join(
|
|
174
|
-
pool: join(raiz, "src", "pool.ts"),
|
|
175
|
-
ayuda: join(raiz, "src", "resources", "grid.ts"),
|
|
183
|
+
sql,
|
|
184
|
+
recurso: archivoRecurso,
|
|
185
|
+
index: join(dirRutas, "index.ts"),
|
|
186
|
+
nuevo: join(dirRutas, "new.ts"),
|
|
187
|
+
editar: join(dirRutas, "[id].ts"),
|
|
188
|
+
rejilla: join(dirRutas, "grid.ts"),
|
|
176
189
|
};
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
if (reusa) {
|
|
191
|
+
delete archivos.sql;
|
|
192
|
+
delete archivos.recurso;
|
|
193
|
+
}
|
|
194
|
+
const existentes = Object.values(archivos).filter((f) => existsSync(f));
|
|
195
|
+
if (existentes.length && !force) {
|
|
196
|
+
const lista = existentes.map((f) => relative(p.raiz, f)).join(", ");
|
|
197
|
+
throw new ScaffoldError(t(`ya existen: ${lista}. Usa --force para reescribirlos`, `already exist: ${lista}. Use --force to overwrite them`));
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
const r = rutas(tabla, a, n, {
|
|
185
|
-
db: { index: importar(archivos.index, out), nested: importar(archivos.nuevo, out) },
|
|
186
|
-
pool: { index: importar(archivos.index,
|
|
187
|
-
resource: { nested: importar(archivos.nuevo,
|
|
188
|
-
base
|
|
201
|
+
db: { index: importar(archivos.index, p.out), nested: importar(archivos.nuevo, p.out) },
|
|
202
|
+
pool: { index: importar(archivos.index, p.pool), nested: importar(archivos.nuevo, p.pool) },
|
|
203
|
+
resource: { nested: importar(archivos.nuevo, archivoRecurso) },
|
|
204
|
+
base,
|
|
189
205
|
});
|
|
190
206
|
const contenidos = {
|
|
191
|
-
sql: consultasSql(tabla, a, n),
|
|
192
|
-
recurso: recurso(tabla, a, n, { db: importar(archivos.recurso, out), enums }),
|
|
193
207
|
index: r.index,
|
|
194
208
|
nuevo: r.nuevo,
|
|
195
209
|
editar: r.editar,
|
|
196
210
|
rejilla: rejilla(tabla, a, n, {
|
|
197
|
-
db: importar(archivos.rejilla, out),
|
|
198
|
-
pool: importar(archivos.rejilla,
|
|
199
|
-
resource: importar(archivos.rejilla,
|
|
200
|
-
grid: importar(archivos.rejilla,
|
|
201
|
-
base
|
|
211
|
+
db: importar(archivos.rejilla, p.out),
|
|
212
|
+
pool: importar(archivos.rejilla, p.pool),
|
|
213
|
+
resource: importar(archivos.rejilla, archivoRecurso),
|
|
214
|
+
grid: importar(archivos.rejilla, p.ayuda),
|
|
215
|
+
base,
|
|
202
216
|
enums,
|
|
203
217
|
}),
|
|
204
218
|
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
writeFileSync(archivos[k], contenido);
|
|
209
|
-
escritos.push(archivos[k]);
|
|
219
|
+
if (!reusa) {
|
|
220
|
+
contenidos.sql = consultasSql(tabla, a, n);
|
|
221
|
+
contenidos.recurso = recurso(tabla, a, n, { db: importar(archivoRecurso, p.out), enums });
|
|
210
222
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
return { tabla: tabla.name, base, archivos, contenidos, reusa };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Escribe los planes y lo común (el pool, el ayudante de la grilla); devuelve lo escrito. */
|
|
227
|
+
function escribir(p, planes, extra = {}) {
|
|
228
|
+
const escritos = [];
|
|
229
|
+
const poner = (archivo, contenido) => {
|
|
230
|
+
mkdirSync(dirname(archivo), { recursive: true });
|
|
231
|
+
writeFileSync(archivo, contenido);
|
|
232
|
+
escritos.push(archivo);
|
|
233
|
+
};
|
|
234
|
+
for (const plan of planes) {
|
|
235
|
+
for (const [k, contenido] of Object.entries(plan.contenidos)) poner(plan.archivos[k], contenido);
|
|
218
236
|
}
|
|
237
|
+
for (const [archivo, contenido] of Object.entries(extra)) poner(archivo, contenido);
|
|
238
|
+
if (!existsSync(p.ayuda)) poner(p.ayuda, ayudaRejilla());
|
|
239
|
+
if (!existsSync(p.pool)) poner(p.pool, piscina({ mysql: p.mysql, migrate: p.config.migrate, db: importar(p.pool, p.out) }));
|
|
240
|
+
return escritos;
|
|
241
|
+
}
|
|
219
242
|
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
/** El módulo de brasa con las consultas nuevas, y lo que falta en el proyecto. */
|
|
244
|
+
function terminar(p, escritos, listo) {
|
|
245
|
+
const generado = spawnSync(process.execPath, [p.brasa.bin, "generate", "--config", p.rutaConfig], { cwd: p.raiz, stdio: "inherit" });
|
|
222
246
|
if (generado.status !== 0) {
|
|
223
247
|
throw new ScaffoldError(t("brasa no pudo generar el módulo con las consultas nuevas (arriba, el error)", "brasa could not generate the module with the new queries (the error is above)"));
|
|
224
248
|
}
|
|
225
|
-
|
|
226
249
|
for (const f of escritos) console.log(` ${relative(process.cwd(), f)}`);
|
|
227
|
-
const paquete = existsSync(join(raiz, "package.json")) ? JSON.parse(readFileSync(join(raiz, "package.json"), "utf8")) : {};
|
|
250
|
+
const paquete = existsSync(join(p.raiz, "package.json")) ? JSON.parse(readFileSync(join(p.raiz, "package.json"), "utf8")) : {};
|
|
228
251
|
const deps = { ...paquete.dependencies, ...paquete.devDependencies };
|
|
229
|
-
const faltan = (mysql ? ["mysql2"] : ["pg", "@types/pg"]).concat("@types/node").filter((d) => !deps[d]);
|
|
252
|
+
const faltan = (p.mysql ? ["mysql2"] : ["pg", "@types/pg"]).concat("@types/node").filter((d) => !deps[d]);
|
|
230
253
|
console.log("");
|
|
231
|
-
console.log(
|
|
254
|
+
console.log(listo);
|
|
232
255
|
if (faltan.length) console.log(t(`Falta instalar: stil add ${faltan.join(" ")}`, `Still to install: npm i ${faltan.join(" ")}`));
|
|
233
|
-
if (!/mode:\s*["']server["']/.test(leer(join(raiz, "vite.config.ts")))) {
|
|
256
|
+
if (!/mode:\s*["']server["']/.test(leer(join(p.raiz, "vite.config.ts")))) {
|
|
234
257
|
console.log(t('Las acciones necesitan servidor: ascua({ site: { mode: "server" } }) en vite.config.ts', 'Actions need a server: ascua({ site: { mode: "server" } }) in vite.config.ts'));
|
|
235
258
|
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** `--admin`: todas las tablas bajo /admin, con su panel y su contraseña. */
|
|
262
|
+
function admin(p, args) {
|
|
263
|
+
const planes = [];
|
|
264
|
+
const saltadas = [];
|
|
265
|
+
const conservadas = [];
|
|
266
|
+
for (const tabla of p.salida.tables) {
|
|
267
|
+
// Una tabla que ya tiene su página en el admin se queda como está.
|
|
268
|
+
const carpeta = tabla.name.replace(/_/g, "-").toLowerCase();
|
|
269
|
+
if (!args.force && existsSync(join(p.raiz, "src", "routes", "admin", carpeta, "index.ts"))) {
|
|
270
|
+
conservadas.push({ name: tabla.name, path: `/admin/${carpeta}` });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
planes.push(planTabla(p, tabla, { prefijo: "admin", force: args.force, reutilizar: true }));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!(error instanceof ScaffoldError)) throw error;
|
|
277
|
+
saltadas.push({ name: tabla.name, reason: error.message });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (planes.length === 0 && conservadas.length === 0) {
|
|
281
|
+
const motivos = saltadas.map((x) => `\n ${x.name}: ${x.reason}`).join("");
|
|
282
|
+
throw new ScaffoldError(t(`ninguna tabla se pudo generar:${motivos}`, `no table could be scaffolded:${motivos}`));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const dir = join(p.raiz, "src", "routes", "admin");
|
|
286
|
+
const datos = join(p.raiz, "src", "resources", "admin.ts");
|
|
287
|
+
// En el orden de las migraciones, las de antes y las nuevas.
|
|
288
|
+
const orden = p.salida.tables.map((x) => x.name);
|
|
289
|
+
const incluidas = [...conservadas, ...planes.map((x) => ({ name: x.tabla, path: x.base }))].sort((a, b) => orden.indexOf(a.name) - orden.indexOf(b.name));
|
|
290
|
+
const extra = { [datos]: datosAdmin(incluidas, saltadas) };
|
|
291
|
+
const propios = {
|
|
292
|
+
[join(dir, "_middleware.ts")]: guardiaAdmin(),
|
|
293
|
+
[join(dir, "_layout.ts")]: marcoAdmin(importar(join(dir, "_layout.ts"), datos)),
|
|
294
|
+
[join(dir, "index.ts")]: panelAdmin(importar(join(dir, "index.ts"), datos)),
|
|
295
|
+
};
|
|
296
|
+
// El panel, la barra y la contraseña son tuyos: se escriben si faltan.
|
|
297
|
+
for (const [archivo, contenido] of Object.entries(propios)) {
|
|
298
|
+
if (args.force || !existsSync(archivo)) extra[archivo] = contenido;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const escritos = escribir(p, planes, extra);
|
|
302
|
+
terminar(p, escritos, t(`Listo: /admin con ${incluidas.length} tablas (${planes.length} nuevas).`, `Done: /admin with ${incluidas.length} tables (${planes.length} new).`));
|
|
303
|
+
for (const plan of planes.filter((x) => x.reusa)) {
|
|
304
|
+
console.log(t(` ${plan.tabla}: usa las consultas y el formulario que ya tenía`, ` ${plan.tabla}: uses the queries and form it already had`));
|
|
305
|
+
}
|
|
306
|
+
for (const x of saltadas) console.log(t(` fuera: ${x.name}: ${x.reason}`, ` left out: ${x.name}: ${x.reason}`));
|
|
307
|
+
console.log(t("Pide usuario y contraseña: define ADMIN_PASSWORD (y ADMIN_USER, «admin» por defecto). Sin ella, /admin es un 404.", "It asks for a user and password: set ADMIN_PASSWORD (and ADMIN_USER, \"admin\" by default). Without it, /admin is a 404."));
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
312
|
+
const args = leerArgumentos(argv);
|
|
313
|
+
if (args.ayuda || (!args.table && !args.admin)) {
|
|
314
|
+
console.log(AYUDA);
|
|
315
|
+
return args.ayuda ? 0 : 1;
|
|
316
|
+
}
|
|
317
|
+
const p = await proyecto(args);
|
|
318
|
+
if (args.admin) return admin(p, args);
|
|
319
|
+
|
|
320
|
+
const tabla = p.salida.tables.find((x) => x.name === args.table) ?? p.salida.tables.find((x) => x.name.toLowerCase() === args.table.toLowerCase());
|
|
321
|
+
if (!tabla) {
|
|
322
|
+
const hay = p.salida.tables.filter((x) => !x.view).map((x) => x.name);
|
|
323
|
+
throw new ScaffoldError(t(`no hay una tabla \`${args.table}\` en las migraciones. Hay: ${hay.join(", ") || "ninguna"}`, `there is no \`${args.table}\` table in the migrations. There are: ${hay.join(", ") || "none"}`));
|
|
324
|
+
}
|
|
325
|
+
const plan = planTabla(p, tabla, { prefijo: "", force: args.force, reutilizar: false });
|
|
326
|
+
terminar(p, escribir(p, [plan]), t(`Listo: ${plan.base} en el sitio.`, `Done: ${plan.base} on the site.`));
|
|
236
327
|
return 0;
|
|
237
328
|
}
|
|
238
329
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ascua-scaffold",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "De una tabla de brasa a un recurso de Ascua: consultas, formulario validado y rutas para listar, crear, editar y borrar.",
|
|
5
5
|
"keywords": ["ascua", "scaffold", "crud", "brasa", "sql", "forms"],
|
|
6
6
|
"homepage": "https://ascua.gitweave.run",
|