ascua-scaffold 0.1.0 → 0.2.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 +17 -0
- package/generate.js +212 -1
- package/index.js +31 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ src/resources/products.ts el esquema del formulario, sus campos y las conv
|
|
|
37
37
|
src/routes/products/index.ts /products, de 50 en 50
|
|
38
38
|
src/routes/products/new.ts /products/new
|
|
39
39
|
src/routes/products/[id].ts /products/:id, editar y borrar (?/delete)
|
|
40
|
+
src/routes/products/grid.ts /products/grid, todas las filas editables a la vez
|
|
41
|
+
src/resources/grid.ts leer lo que manda una grilla (uno para todo el sitio)
|
|
40
42
|
src/pool.ts el pool, con las migraciones aplicadas una vez (si no existe)
|
|
41
43
|
```
|
|
42
44
|
|
|
@@ -66,6 +68,21 @@ obligatoria y no tiene `DEFAULT`, el comando lo dice y no genera nada.
|
|
|
66
68
|
Lo generado es tuyo: no se vuelve a generar. Con `--force` se reescribe
|
|
67
69
|
encima.
|
|
68
70
|
|
|
71
|
+
## La grilla
|
|
72
|
+
|
|
73
|
+
`/products/grid` es la página entera editable, como una DataWindow de
|
|
74
|
+
PowerBuilder: una fila por registro y una celda por campo, de 50 en 50. Es
|
|
75
|
+
un `<form>` de HTML, así que también funciona sin JavaScript.
|
|
76
|
+
|
|
77
|
+
- Cada fila lleva los valores con que se mostró: al guardar, solo se
|
|
78
|
+
escriben las filas que cambiaron.
|
|
79
|
+
- Cada fila cambiada se valida con el mismo esquema del formulario
|
|
80
|
+
(`ProductForm`) y se guardan todas en una sola transacción
|
|
81
|
+
(`transaction()` de brasa): si una falla, no se escribe ninguna, y el
|
|
82
|
+
error aparece en su celda.
|
|
83
|
+
- Un `UNIQUE` repetido vuelve con un 409 y el error bajo la celda de esa
|
|
84
|
+
fila.
|
|
85
|
+
|
|
69
86
|
## Requisitos
|
|
70
87
|
|
|
71
88
|
- Un sitio con servidor: `ascua({ site: { mode: "server" } })` en
|
package/generate.js
CHANGED
|
@@ -426,7 +426,7 @@ export default function ${n.plural}({ data }: { data: Awaited<ReturnType<typeof
|
|
|
426
426
|
return view\`
|
|
427
427
|
<section>
|
|
428
428
|
<h1>${titulo}</h1>
|
|
429
|
-
<p><a href="${base}/new">New ${uno}</a></p>
|
|
429
|
+
<p><a href="${base}/new">New ${uno}</a> · <a href="${base}/grid">Edit as a grid</a></p>
|
|
430
430
|
<table>
|
|
431
431
|
<thead><tr>${cabeceras}</tr></thead>
|
|
432
432
|
<tbody>
|
|
@@ -554,6 +554,217 @@ export default function Edit${n.uno}({ data, form }: { data: Awaited<ReturnType<
|
|
|
554
554
|
return { index, nuevo, editar };
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
/** `src/resources/grid.ts`: leer lo que manda una grilla. Uno para todo el sitio. */
|
|
558
|
+
export function ayudaRejilla() {
|
|
559
|
+
return `// Generated by ascua-scaffold. Yours to edit: it is not generated again.
|
|
560
|
+
//
|
|
561
|
+
// A grid is a plain <form>: one row per record, one field per cell. Each row
|
|
562
|
+
// carries its key (k0, k1…) and the values it was shown with (w0, w1…), so
|
|
563
|
+
// the server only writes the rows that changed. Cell names are c<row>.<column>.
|
|
564
|
+
|
|
565
|
+
export type FormValues = Partial<Record<string, string | string[]>>;
|
|
566
|
+
export type FormErrors = Partial<Record<string, string>>;
|
|
567
|
+
|
|
568
|
+
/** A row of the grid: its key, as text, and the values of its fields. */
|
|
569
|
+
export interface GridRow {
|
|
570
|
+
key: string;
|
|
571
|
+
values: FormValues;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Every row the grid sent, and the ones that changed. */
|
|
575
|
+
export function readGrid(formData: FormData, columns: readonly string[]): { rows: GridRow[]; changed: GridRow[] } {
|
|
576
|
+
const rows: GridRow[] = [];
|
|
577
|
+
const changed: GridRow[] = [];
|
|
578
|
+
for (let i = 0; formData.has(\`k\${i}\`); i++) {
|
|
579
|
+
const key = String(formData.get(\`k\${i}\`));
|
|
580
|
+
const values: FormValues = {};
|
|
581
|
+
// An unchecked checkbox sends nothing, and stays missing: field.checkbox()
|
|
582
|
+
// takes any value that arrived, even "", as checked.
|
|
583
|
+
for (const column of columns) {
|
|
584
|
+
const value = formData.get(\`c\${i}.\${column}\`);
|
|
585
|
+
if (typeof value === "string") values[column] = value;
|
|
586
|
+
}
|
|
587
|
+
let was: FormValues = {};
|
|
588
|
+
try {
|
|
589
|
+
was = JSON.parse(String(formData.get(\`w\${i}\`) ?? "{}"));
|
|
590
|
+
} catch {
|
|
591
|
+
// Without what it was shown with, the row counts as changed.
|
|
592
|
+
}
|
|
593
|
+
rows.push({ key, values });
|
|
594
|
+
if (columns.some((column) => (was[column] ?? "") !== (values[column] ?? ""))) changed.push({ key, values });
|
|
595
|
+
}
|
|
596
|
+
return { rows, changed };
|
|
597
|
+
}
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Una celda editable de la grilla, con su error debajo. */
|
|
602
|
+
function celda(c, valoresEnum) {
|
|
603
|
+
const nombre = `\${\`c\${r.index}.${c.name}\`}`;
|
|
604
|
+
const v = `r.values${acceso(c.name)}`;
|
|
605
|
+
const texto = `String(${v} ?? "")`;
|
|
606
|
+
let html;
|
|
607
|
+
switch (c.kind) {
|
|
608
|
+
case "boolean":
|
|
609
|
+
html = `<input type="checkbox" name=${nombre} prop:checked=\${${v} === "on"}>`;
|
|
610
|
+
break;
|
|
611
|
+
case "enum": {
|
|
612
|
+
const vacia = c.nullable ? `<option value=""></option>` : "";
|
|
613
|
+
html =
|
|
614
|
+
`<select name=${nombre}>${vacia}<For each=\${() => ${valoresEnum}} key=\${(v: string) => v} ` +
|
|
615
|
+
`render=\${(v: string) => view\`<option prop:selected=\${${v} === v}>\${v}</option>\`}/></select>`;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case "integer":
|
|
619
|
+
html = `<input type="number" step="1" name=${nombre} prop:value=\${${texto}}>`;
|
|
620
|
+
break;
|
|
621
|
+
case "decimal":
|
|
622
|
+
case "number":
|
|
623
|
+
html = `<input type="number" step="any" name=${nombre} prop:value=\${${texto}}>`;
|
|
624
|
+
break;
|
|
625
|
+
case "date":
|
|
626
|
+
html = `<input type="date" name=${nombre} prop:value=\${${texto}}>`;
|
|
627
|
+
break;
|
|
628
|
+
case "datetime":
|
|
629
|
+
html = `<input type="datetime-local" name=${nombre} prop:value=\${${texto}}>`;
|
|
630
|
+
break;
|
|
631
|
+
case "time":
|
|
632
|
+
html = `<input type="time" name=${nombre} prop:value=\${${texto}}>`;
|
|
633
|
+
break;
|
|
634
|
+
default:
|
|
635
|
+
html = `<input name=${nombre} prop:value=\${${texto}}>`;
|
|
636
|
+
}
|
|
637
|
+
return `<td>${html}<small class="error">\${r.errors${acceso(c.name)} ?? ""}</small></td>`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Los valores de un enum en el código generado: `<x>Values` o escritos. */
|
|
641
|
+
export function valoresEnumDe(c, enums) {
|
|
642
|
+
return c.values ? (enums.has(c.sqlType) ? `${camel(c.sqlType)}Values` : JSON.stringify(c.values)) : "[]";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** `src/routes/<tabla>/grid.ts`: la página entera editable, como una DataWindow. */
|
|
646
|
+
export function rejilla(tabla, a, n, { db, pool, resource, grid, base, enums }) {
|
|
647
|
+
const { guarda, valor } = claveDeUrl(a);
|
|
648
|
+
const pk = a.clave.name;
|
|
649
|
+
const titulo = etiqueta(tabla.name);
|
|
650
|
+
const forma = `${n.uno}Form`;
|
|
651
|
+
const params = `${camel(n.uno)}Params`;
|
|
652
|
+
const values = `${camel(n.uno)}Values`;
|
|
653
|
+
const importsEnum = [...new Set(a.alEditar.filter((c) => c.kind === "enum" && enums.has(c.sqlType)).map((c) => `${camel(c.sqlType)}Values`))];
|
|
654
|
+
const columnas = a.alEditar.map((c) => JSON.stringify(c.name)).join(", ");
|
|
655
|
+
const cabeceras = [pk, ...a.alEditar.map((c) => c.name)].map((c) => `<th>${etiqueta(c)}</th>`).join("");
|
|
656
|
+
const celdas = a.alEditar.map((c) => celda(c, valoresEnumDe(c, enums))).join("\n ");
|
|
657
|
+
|
|
658
|
+
return `// Generated by ascua-scaffold. Yours to edit: it is not generated again.
|
|
659
|
+
//
|
|
660
|
+
// Every ${singular(tabla.name)} of the page, editable at once. Saving validates each changed
|
|
661
|
+
// row with ${forma} and writes them all in one transaction: if one fails,
|
|
662
|
+
// none is written, and the error shows in its cell.
|
|
663
|
+
import { fail, notFound, redirect, validate, type ActionContext, type LoadContext } from "vite-plugin-ascua/site";
|
|
664
|
+
|
|
665
|
+
import { ${[...importsEnum, camel(n.list), "transaction", camel(n.update)].join(", ")} } from "${db}";
|
|
666
|
+
import { db } from "${pool}";
|
|
667
|
+
import { readGrid, type FormErrors, type FormValues } from "${grid}";
|
|
668
|
+
import { ${forma}, ${params}, ${values}, duplicate, type ${n.uno}Data } from "${resource}";
|
|
669
|
+
|
|
670
|
+
const PAGE = 50;
|
|
671
|
+
const COLUMNS = [${columnas}] as const;
|
|
672
|
+
|
|
673
|
+
export const title = "${titulo}";
|
|
674
|
+
|
|
675
|
+
/** The key from the form, or a 404 if it cannot be one. */
|
|
676
|
+
function key(id: string) {
|
|
677
|
+
if (!(${guarda})) throw notFound();
|
|
678
|
+
return ${valor};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export async function load(_params: Record<string, string>, { url }: LoadContext) {
|
|
682
|
+
const page = Math.max(1, Math.floor(Number(url.searchParams.get("page"))) || 1);
|
|
683
|
+
const rows = await ${camel(n.list)}(await db(), { limit: PAGE + 1, offset: (page - 1) * PAGE });
|
|
684
|
+
return {
|
|
685
|
+
page,
|
|
686
|
+
more: rows.length > PAGE,
|
|
687
|
+
rows: rows.slice(0, PAGE).map((row) => ({ key: String(row${acceso(pk)}), values: ${values}(row) })),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** What a failed save sends back: the errors and what was written, by row key. */
|
|
692
|
+
interface Failed {
|
|
693
|
+
errors: Record<string, FormErrors>;
|
|
694
|
+
values: Record<string, FormValues>;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export const actions = {
|
|
698
|
+
async default({ formData, url }: ActionContext) {
|
|
699
|
+
const { rows, changed } = readGrid(formData, COLUMNS);
|
|
700
|
+
const values = Object.fromEntries(rows.map((row) => [row.key, row.values]));
|
|
701
|
+
const errors: Record<string, FormErrors> = {};
|
|
702
|
+
const valid: { key: string; data: ${n.uno}Data }[] = [];
|
|
703
|
+
for (const row of changed) {
|
|
704
|
+
const result = await validate(${forma}, row.values);
|
|
705
|
+
if (result.ok) valid.push({ key: row.key, data: result.data });
|
|
706
|
+
else errors[row.key] = result.errors;
|
|
707
|
+
}
|
|
708
|
+
if (Object.keys(errors).length) return fail(400, { errors, values } satisfies Failed);
|
|
709
|
+
|
|
710
|
+
let current = "";
|
|
711
|
+
try {
|
|
712
|
+
await transaction(await db(), async (tx) => {
|
|
713
|
+
for (const row of valid) {
|
|
714
|
+
current = row.key;
|
|
715
|
+
await ${camel(n.update)}(tx, { ...${params}(row.data), ${propiedad(pk)}: key(row.key) });
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
} catch (error) {
|
|
719
|
+
// A repeated UNIQUE value: the error goes under that row's cell.
|
|
720
|
+
const taken = duplicate(error, new FormData());
|
|
721
|
+
return fail(409, { errors: { [current]: taken.data.errors }, values } satisfies Failed);
|
|
722
|
+
}
|
|
723
|
+
throw redirect(url.pathname + url.search);
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
export default function ${n.plural}Grid({ data, form }: { data: Awaited<ReturnType<typeof load>>; form?: Failed }) {
|
|
728
|
+
const rows = data.rows.map((row, index) => ({
|
|
729
|
+
index,
|
|
730
|
+
key: row.key,
|
|
731
|
+
was: JSON.stringify(row.values),
|
|
732
|
+
values: form?.values[row.key] ?? row.values,
|
|
733
|
+
errors: form?.errors[row.key] ?? {},
|
|
734
|
+
}));
|
|
735
|
+
type Row = (typeof rows)[number];
|
|
736
|
+
return view\`
|
|
737
|
+
<section>
|
|
738
|
+
<h1>${titulo}</h1>
|
|
739
|
+
<p><a href="${base}">List</a> · <a href="${base}/new">New ${etiqueta(singular(tabla.name)).toLowerCase()}</a></p>
|
|
740
|
+
<form method="post">
|
|
741
|
+
<table class="grid">
|
|
742
|
+
<thead><tr>${cabeceras}</tr></thead>
|
|
743
|
+
<tbody>
|
|
744
|
+
<For each=\${() => rows} key=\${(r: Row) => r.key}
|
|
745
|
+
render=\${(r: Row) => view\`<tr>
|
|
746
|
+
<td><a href=\${\`${base}/\${r.key}\`}>\${r.key}</a><input type="hidden" name=\${\`k\${r.index}\`} value=\${r.key}><input type="hidden" name=\${\`w\${r.index}\`} value=\${r.was}><small class="error">\${r.errors[""] ?? ""}</small></td>
|
|
747
|
+
${celdas}
|
|
748
|
+
</tr>\`}/>
|
|
749
|
+
</tbody>
|
|
750
|
+
</table>
|
|
751
|
+
<button>Save changes</button>
|
|
752
|
+
</form>
|
|
753
|
+
<nav>
|
|
754
|
+
<Show when=\${() => data.page > 1}><a href=\${\`?page=\${data.page - 1}\`}>Previous</a></Show>
|
|
755
|
+
<Show when=\${() => data.more}><a href=\${\`?page=\${data.page + 1}\`}>Next</a></Show>
|
|
756
|
+
</nav>
|
|
757
|
+
<style>
|
|
758
|
+
.grid input, .grid select { width: 100%; box-sizing: border-box; }
|
|
759
|
+
.grid input[type="checkbox"] { width: auto; }
|
|
760
|
+
.grid td { vertical-align: top; }
|
|
761
|
+
.error { display: block; color: crimson; }
|
|
762
|
+
</style>
|
|
763
|
+
</section>\`;
|
|
764
|
+
}
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
|
|
557
768
|
/** `src/pool.ts`: un pool para todo el servidor, con las migraciones aplicadas una vez. */
|
|
558
769
|
export function piscina({ mysql, migrate, db }) {
|
|
559
770
|
if (mysql) {
|
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ 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, consultasSql, nombres, piscina, recurso, rutas, ScaffoldError, t } from "./generate.js";
|
|
19
|
+
import { analizar, ayudaRejilla, consultasSql, nombres, piscina, recurso, rejilla, rutas, ScaffoldError, t } from "./generate.js";
|
|
20
20
|
|
|
21
21
|
const AYUDA = t(
|
|
22
22
|
`Uso: ascua-scaffold <tabla> [opciones]
|
|
@@ -27,6 +27,8 @@ Genera, a partir de una tabla de las migraciones de brasa:
|
|
|
27
27
|
src/routes/<tabla>/index.ts la lista, paginada
|
|
28
28
|
src/routes/<tabla>/new.ts el alta
|
|
29
29
|
src/routes/<tabla>/[id].ts la edición y la baja
|
|
30
|
+
src/routes/<tabla>/grid.ts todas las filas editables a la vez
|
|
31
|
+
src/resources/grid.ts leer lo que manda una grilla, si no existe
|
|
30
32
|
src/pool.ts el pool de la base, si no existe
|
|
31
33
|
|
|
32
34
|
Opciones:
|
|
@@ -41,6 +43,8 @@ Generates, from a table in the brasa migrations:
|
|
|
41
43
|
src/routes/<table>/index.ts the list, paginated
|
|
42
44
|
src/routes/<table>/new.ts creating
|
|
43
45
|
src/routes/<table>/[id].ts editing and deleting
|
|
46
|
+
src/routes/<table>/grid.ts every row editable at once
|
|
47
|
+
src/resources/grid.ts reading what a grid sends, if missing
|
|
44
48
|
src/pool.ts the database pool, if missing
|
|
45
49
|
|
|
46
50
|
Options:
|
|
@@ -138,6 +142,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
138
142
|
const propio = join(dirConsultas, `${tabla.name}.sql`);
|
|
139
143
|
const ajenas = salida.queries.filter((x) => !(args.force && resolve(x.file) === propio));
|
|
140
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
|
+
}
|
|
141
153
|
if (repetidas.length) {
|
|
142
154
|
throw new ScaffoldError(
|
|
143
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`),
|
|
@@ -145,6 +157,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
const carpeta = tabla.name.replace(/_/g, "-").toLowerCase();
|
|
160
|
+
if (carpeta === "grid") {
|
|
161
|
+
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
|
+
}
|
|
148
163
|
// Los enums de Postgres tienen su `<x>Values` en el módulo; los de MySQL
|
|
149
164
|
// (`ENUM('a','b')` en la columna), no: sus valores van escritos.
|
|
150
165
|
const enums = new Set(mysql ? [] : tabla.columns.filter((c) => c.kind === "enum").map((c) => c.sqlType));
|
|
@@ -155,9 +170,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
155
170
|
index: join(raiz, "src", "routes", carpeta, "index.ts"),
|
|
156
171
|
nuevo: join(raiz, "src", "routes", carpeta, "new.ts"),
|
|
157
172
|
editar: join(raiz, "src", "routes", carpeta, "[id].ts"),
|
|
173
|
+
rejilla: join(raiz, "src", "routes", carpeta, "grid.ts"),
|
|
158
174
|
pool: join(raiz, "src", "pool.ts"),
|
|
175
|
+
ayuda: join(raiz, "src", "resources", "grid.ts"),
|
|
159
176
|
};
|
|
160
|
-
const existentes = Object.entries(archivos).filter(([k, f]) => k !== "pool" && existsSync(f));
|
|
177
|
+
const existentes = Object.entries(archivos).filter(([k, f]) => k !== "pool" && k !== "ayuda" && existsSync(f));
|
|
161
178
|
if (existentes.length && !args.force) {
|
|
162
179
|
throw new ScaffoldError(
|
|
163
180
|
t(`ya existen: ${existentes.map(([, f]) => relative(raiz, f)).join(", ")}. Usa --force para reescribirlos`, `already exist: ${existentes.map(([, f]) => relative(raiz, f)).join(", ")}. Use --force to overwrite them`),
|
|
@@ -176,6 +193,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
176
193
|
index: r.index,
|
|
177
194
|
nuevo: r.nuevo,
|
|
178
195
|
editar: r.editar,
|
|
196
|
+
rejilla: rejilla(tabla, a, n, {
|
|
197
|
+
db: importar(archivos.rejilla, out),
|
|
198
|
+
pool: importar(archivos.rejilla, archivos.pool),
|
|
199
|
+
resource: importar(archivos.rejilla, archivos.recurso),
|
|
200
|
+
grid: importar(archivos.rejilla, archivos.ayuda),
|
|
201
|
+
base: `/${carpeta}`,
|
|
202
|
+
enums,
|
|
203
|
+
}),
|
|
179
204
|
};
|
|
180
205
|
const escritos = [];
|
|
181
206
|
for (const [k, contenido] of Object.entries(contenidos)) {
|
|
@@ -183,6 +208,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
183
208
|
writeFileSync(archivos[k], contenido);
|
|
184
209
|
escritos.push(archivos[k]);
|
|
185
210
|
}
|
|
211
|
+
if (!existsSync(archivos.ayuda)) {
|
|
212
|
+
writeFileSync(archivos.ayuda, ayudaRejilla());
|
|
213
|
+
escritos.push(archivos.ayuda);
|
|
214
|
+
}
|
|
186
215
|
if (!existsSync(archivos.pool)) {
|
|
187
216
|
writeFileSync(archivos.pool, piscina({ mysql, migrate: config.migrate, db: importar(archivos.pool, out) }));
|
|
188
217
|
escritos.push(archivos.pool);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ascua-scaffold",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|