create-softeneers-app 0.2.0 → 0.2.2
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/scaffold.js +24 -1
- package/dist/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/templates/express-api/README.md +10 -10
- package/templates/express-api/gitignore +7 -0
- package/templates/express-api/softeneers.template.json +1 -1
- package/templates/express-api/src/cars/demo.ts +10 -0
- package/templates/express-api/src/cars/store.ts +100 -42
- package/templates/express-api/src/index.ts +2 -20
- package/templates/express-api/src/scripts/seed.ts +7 -12
- package/templates/hono-api/README.md +10 -10
- package/templates/hono-api/gitignore +7 -0
- package/templates/hono-api/softeneers.template.json +1 -1
- package/templates/hono-api/src/cars/demo.ts +10 -0
- package/templates/hono-api/src/cars/store.ts +100 -42
- package/templates/hono-api/src/index.ts +2 -20
- package/templates/hono-api/src/scripts/seed.ts +7 -12
- package/templates/minimal/gitignore +7 -0
- package/templates/next-fullstack/apps/web/gitignore +41 -0
- package/templates/next-fullstack/gitignore +11 -0
- package/templates/tanstack-start/README.md +10 -9
- package/templates/tanstack-start/gitignore +16 -0
- package/templates/tanstack-start/softeneers.template.json +1 -1
- package/templates/tanstack-start/src/server/demo.ts +9 -0
- package/templates/tanstack-start/src/server/scripts/seed.ts +7 -12
- package/templates/tanstack-start/src/server/store.ts +85 -32
package/dist/scaffold.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { CliError } from "./args.js";
|
|
5
5
|
/** Names excluded when copying a template (docs/CLI-SPEC.md → copy exclusions). */
|
|
@@ -101,6 +101,28 @@ function generateEnvFiles(targetDir) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Restore `.gitignore` files. Templates ship them as `gitignore` (no dot) because
|
|
106
|
+
* npm strips a real `.gitignore` from a published package; on generation we rename
|
|
107
|
+
* each back so the project ignores node_modules/.env/etc. from the first commit.
|
|
108
|
+
*/
|
|
109
|
+
function restoreGitignores(targetDir) {
|
|
110
|
+
const stack = [targetDir];
|
|
111
|
+
while (stack.length) {
|
|
112
|
+
const dir = stack.pop();
|
|
113
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
if (entry.name === "node_modules")
|
|
115
|
+
continue;
|
|
116
|
+
const full = join(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
stack.push(full);
|
|
119
|
+
}
|
|
120
|
+
else if (entry.name === "gitignore") {
|
|
121
|
+
renameSync(full, join(dir, ".gitignore"));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
104
126
|
/** Replace {{PROJECT_NAME}} placeholders in the README. */
|
|
105
127
|
function substitutePlaceholders(targetDir, projectName) {
|
|
106
128
|
const readme = join(targetDir, "README.md");
|
|
@@ -110,6 +132,7 @@ function substitutePlaceholders(targetDir, projectName) {
|
|
|
110
132
|
writeFileSync(readme, next);
|
|
111
133
|
}
|
|
112
134
|
export function transform(targetDir, projectName, pkgName, pm) {
|
|
135
|
+
restoreGitignores(targetDir);
|
|
113
136
|
rewriteRootPackage(targetDir, pkgName, pm);
|
|
114
137
|
generateEnvFiles(targetDir);
|
|
115
138
|
substitutePlaceholders(targetDir, projectName);
|
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-softeneers-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Scaffold a new Softeneers Framework project: 5 templates (next-fullstack, express-api, hono-api, tanstack-start, minimal) with db/auth/docker toggles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,17 +43,18 @@ curl -X POST localhost:4000/api/cars -H 'content-type: application/json' \
|
|
|
43
43
|
## Database
|
|
44
44
|
|
|
45
45
|
Persistence is MySQL via Sequelize ([`@softeneers/db`](https://www.npmjs.com/package/@softeneers/db)).
|
|
46
|
-
|
|
46
|
+
**`npm run dev` works immediately even without a database** — the store falls back
|
|
47
|
+
to a pre-seeded in-memory one when MySQL is unreachable. To switch on real
|
|
48
|
+
persistence, set `DB_*` in `.env` and start MySQL:
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
51
|
# #if docker
|
|
50
52
|
docker compose up -d # start MySQL
|
|
51
53
|
# #endif
|
|
54
|
+
# The app auto-creates and seeds tables on first connect; these scripts give
|
|
55
|
+
# you explicit control (e.g. reseeding or a clean reset):
|
|
52
56
|
npm run db:migrate && npm run db:seed
|
|
53
57
|
```
|
|
54
|
-
|
|
55
|
-
Toggle the database off at generation time (`--no-db`) to swap in a dependency-free
|
|
56
|
-
in-memory store — the CRUD API behaves identically.
|
|
57
58
|
# #endif
|
|
58
59
|
# #if auth
|
|
59
60
|
|
|
@@ -67,11 +68,10 @@ mounted at `/api/auth/*`. Set a strong `AUTH_SECRET` in `.env` before deploying.
|
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
npm install
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# #endif
|
|
71
|
+
npm run dev # a pre-seeded CRUD demo — no other setup needed
|
|
72
|
+
```
|
|
73
73
|
# #if db
|
|
74
|
-
|
|
74
|
+
|
|
75
|
+
The app starts immediately on an in-memory demo and uses MySQL automatically once
|
|
76
|
+
it's reachable — see **Database** above to enable persistence.
|
|
75
77
|
# #endif
|
|
76
|
-
npm run dev
|
|
77
|
-
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NewCar } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// Demo garage inventory seeded on first run so `npm run dev` shows a working
|
|
4
|
+
// CRUD demo immediately — into MySQL when a database is reachable, otherwise
|
|
5
|
+
// into the in-memory store.
|
|
6
|
+
export const DEMO_CARS: NewCar[] = [
|
|
7
|
+
{ brand: "Toyota", model: "Corolla", year: 2021 },
|
|
8
|
+
{ brand: "Tesla", model: "Model 3", year: 2023 },
|
|
9
|
+
{ brand: "Ford", model: "Mustang", year: 1969 },
|
|
10
|
+
];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { DEMO_CARS } from "./demo.js";
|
|
1
2
|
import type { Car, NewCar } from "./types.js";
|
|
2
3
|
|
|
3
|
-
// The data layer behind the CRUD routes. With the `db` toggle on
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
4
|
+
// The data layer behind the CRUD routes. With the `db` toggle on it persists to
|
|
5
|
+
// MySQL (Sequelize via @softeneers/db) and **falls back to an in-memory store if
|
|
6
|
+
// the database is unreachable**, so `npm run dev` always yields a working,
|
|
7
|
+
// pre-seeded demo. With `db` off it is always in-memory.
|
|
6
8
|
export interface CarStore {
|
|
7
9
|
list(): Promise<Car[]>;
|
|
8
10
|
get(id: number): Promise<Car | null>;
|
|
@@ -11,8 +13,43 @@ export interface CarStore {
|
|
|
11
13
|
remove(id: number): Promise<boolean>;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
// In-memory backend (the default, and the fallback when no database is reachable).
|
|
17
|
+
function createMemoryStore(seed = true): CarStore {
|
|
18
|
+
let nextId = 1;
|
|
19
|
+
const cars = new Map<number, Car>();
|
|
20
|
+
if (seed) {
|
|
21
|
+
for (const car of DEMO_CARS) {
|
|
22
|
+
const id = nextId++;
|
|
23
|
+
cars.set(id, { id, ...car });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
async list() {
|
|
28
|
+
return [...cars.values()].sort((a, b) => a.id - b.id);
|
|
29
|
+
},
|
|
30
|
+
async get(id) {
|
|
31
|
+
return cars.get(id) ?? null;
|
|
32
|
+
},
|
|
33
|
+
async create(input) {
|
|
34
|
+
const car: Car = { id: nextId++, ...input };
|
|
35
|
+
cars.set(car.id, car);
|
|
36
|
+
return car;
|
|
37
|
+
},
|
|
38
|
+
async update(id, input) {
|
|
39
|
+
const car = cars.get(id);
|
|
40
|
+
if (!car) return null;
|
|
41
|
+
const next: Car = { ...car, ...input };
|
|
42
|
+
cars.set(id, next);
|
|
43
|
+
return next;
|
|
44
|
+
},
|
|
45
|
+
async remove(id) {
|
|
46
|
+
return cars.delete(id);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
// #if db
|
|
15
|
-
import { DataTypes, Model } from "@softeneers/db";
|
|
52
|
+
import { assertConnection, DataTypes, Model } from "@softeneers/db";
|
|
16
53
|
|
|
17
54
|
import { sequelize } from "../db.js";
|
|
18
55
|
|
|
@@ -37,56 +74,77 @@ export { CarModel };
|
|
|
37
74
|
|
|
38
75
|
const toCar = (m: CarModel): Car => ({ id: m.id, brand: m.brand, model: m.model, year: m.year });
|
|
39
76
|
|
|
77
|
+
function createDbStore(): CarStore {
|
|
78
|
+
return {
|
|
79
|
+
async list() {
|
|
80
|
+
return (await CarModel.findAll({ order: [["id", "ASC"]] })).map(toCar);
|
|
81
|
+
},
|
|
82
|
+
async get(id) {
|
|
83
|
+
const m = await CarModel.findByPk(id);
|
|
84
|
+
return m ? toCar(m) : null;
|
|
85
|
+
},
|
|
86
|
+
async create(input) {
|
|
87
|
+
return toCar(await CarModel.create(input));
|
|
88
|
+
},
|
|
89
|
+
async update(id, input) {
|
|
90
|
+
const m = await CarModel.findByPk(id);
|
|
91
|
+
if (!m) return null;
|
|
92
|
+
await m.update(input);
|
|
93
|
+
return toCar(m);
|
|
94
|
+
},
|
|
95
|
+
async remove(id) {
|
|
96
|
+
const m = await CarModel.findByPk(id);
|
|
97
|
+
if (!m) return false;
|
|
98
|
+
await m.destroy();
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resolve the backend once, lazily, on first use: try MySQL (create tables +
|
|
105
|
+
// seed if empty); on any connection error, fall back to the in-memory store.
|
|
106
|
+
let backend: Promise<CarStore> | null = null;
|
|
107
|
+
function resolveBackend(): Promise<CarStore> {
|
|
108
|
+
if (!backend) {
|
|
109
|
+
backend = (async () => {
|
|
110
|
+
try {
|
|
111
|
+
await assertConnection(sequelize);
|
|
112
|
+
await sequelize.sync();
|
|
113
|
+
const db = createDbStore();
|
|
114
|
+
if ((await db.list()).length === 0) {
|
|
115
|
+
for (const car of DEMO_CARS) await db.create(car);
|
|
116
|
+
}
|
|
117
|
+
console.log("Data store: MySQL");
|
|
118
|
+
return db;
|
|
119
|
+
} catch {
|
|
120
|
+
console.warn(
|
|
121
|
+
"Data store: in-memory — database unreachable. Run `docker compose up -d` (and `npm run db:migrate && npm run db:seed`) for MySQL.",
|
|
122
|
+
);
|
|
123
|
+
return createMemoryStore();
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
}
|
|
127
|
+
return backend;
|
|
128
|
+
}
|
|
129
|
+
|
|
40
130
|
export const carStore: CarStore = {
|
|
41
131
|
async list() {
|
|
42
|
-
return (await
|
|
132
|
+
return (await resolveBackend()).list();
|
|
43
133
|
},
|
|
44
134
|
async get(id) {
|
|
45
|
-
|
|
46
|
-
return m ? toCar(m) : null;
|
|
135
|
+
return (await resolveBackend()).get(id);
|
|
47
136
|
},
|
|
48
137
|
async create(input) {
|
|
49
|
-
return
|
|
138
|
+
return (await resolveBackend()).create(input);
|
|
50
139
|
},
|
|
51
140
|
async update(id, input) {
|
|
52
|
-
|
|
53
|
-
if (!m) return null;
|
|
54
|
-
await m.update(input);
|
|
55
|
-
return toCar(m);
|
|
141
|
+
return (await resolveBackend()).update(id, input);
|
|
56
142
|
},
|
|
57
143
|
async remove(id) {
|
|
58
|
-
|
|
59
|
-
if (!m) return false;
|
|
60
|
-
await m.destroy();
|
|
61
|
-
return true;
|
|
144
|
+
return (await resolveBackend()).remove(id);
|
|
62
145
|
},
|
|
63
146
|
};
|
|
64
147
|
// #endif
|
|
65
148
|
// #if !db
|
|
66
|
-
|
|
67
|
-
const cars = new Map<number, Car>();
|
|
68
|
-
|
|
69
|
-
export const carStore: CarStore = {
|
|
70
|
-
async list() {
|
|
71
|
-
return [...cars.values()].sort((a, b) => a.id - b.id);
|
|
72
|
-
},
|
|
73
|
-
async get(id) {
|
|
74
|
-
return cars.get(id) ?? null;
|
|
75
|
-
},
|
|
76
|
-
async create(input) {
|
|
77
|
-
const car: Car = { id: nextId++, ...input };
|
|
78
|
-
cars.set(car.id, car);
|
|
79
|
-
return car;
|
|
80
|
-
},
|
|
81
|
-
async update(id, input) {
|
|
82
|
-
const car = cars.get(id);
|
|
83
|
-
if (!car) return null;
|
|
84
|
-
const next: Car = { ...car, ...input };
|
|
85
|
-
cars.set(id, next);
|
|
86
|
-
return next;
|
|
87
|
-
},
|
|
88
|
-
async remove(id) {
|
|
89
|
-
return cars.delete(id);
|
|
90
|
-
},
|
|
91
|
-
};
|
|
149
|
+
export const carStore: CarStore = createMemoryStore();
|
|
92
150
|
// #endif
|
|
@@ -3,12 +3,6 @@ import express from "express";
|
|
|
3
3
|
|
|
4
4
|
import { carRouter } from "./cars/routes.js";
|
|
5
5
|
import { env } from "./env.js";
|
|
6
|
-
// #if db
|
|
7
|
-
import { assertConnection } from "@softeneers/db";
|
|
8
|
-
|
|
9
|
-
import { sequelize } from "./db.js";
|
|
10
|
-
import "./cars/store.js"; // registers the Car model on the connection
|
|
11
|
-
// #endif
|
|
12
6
|
// #if auth
|
|
13
7
|
import { toNodeHandler } from "@softeneers/auth";
|
|
14
8
|
|
|
@@ -38,18 +32,6 @@ app.use(
|
|
|
38
32
|
},
|
|
39
33
|
);
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await assertConnection(sequelize);
|
|
44
|
-
await sequelize.sync();
|
|
45
|
-
console.log("Database connected.");
|
|
46
|
-
// #endif
|
|
47
|
-
app.listen(env.PORT, () => {
|
|
48
|
-
console.log(`API running on http://localhost:${env.PORT}`);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
start().catch((error) => {
|
|
53
|
-
console.error("Failed to start:", error);
|
|
54
|
-
process.exit(1);
|
|
35
|
+
app.listen(env.PORT, () => {
|
|
36
|
+
console.log(`API running on http://localhost:${env.PORT}`);
|
|
55
37
|
});
|
|
@@ -2,24 +2,19 @@ import "dotenv/config";
|
|
|
2
2
|
|
|
3
3
|
import { assertConnection } from "@softeneers/db";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { DEMO_CARS } from "../cars/demo.js";
|
|
6
|
+
import { CarModel } from "../cars/store.js";
|
|
6
7
|
import { sequelize } from "../db.js";
|
|
7
8
|
|
|
8
9
|
await assertConnection(sequelize);
|
|
9
10
|
await sequelize.sync();
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
-
console.log(`Skipped seeding — ${
|
|
12
|
+
const count = await CarModel.count();
|
|
13
|
+
if (count > 0) {
|
|
14
|
+
console.log(`Skipped seeding — ${count} cars already present.`);
|
|
14
15
|
} else {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ brand: "Tesla", model: "Model 3", year: 2023 },
|
|
18
|
-
{ brand: "Ford", model: "Mustang", year: 1969 },
|
|
19
|
-
]) {
|
|
20
|
-
await carStore.create(car);
|
|
21
|
-
}
|
|
22
|
-
console.log("Seeded 3 cars.");
|
|
16
|
+
await CarModel.bulkCreate([...DEMO_CARS]);
|
|
17
|
+
console.log(`Seeded ${DEMO_CARS.length} cars.`);
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
await sequelize.close();
|
|
@@ -43,17 +43,18 @@ curl -X POST localhost:4000/api/cars -H 'content-type: application/json' \
|
|
|
43
43
|
## Database
|
|
44
44
|
|
|
45
45
|
Persistence is MySQL via Sequelize ([`@softeneers/db`](https://www.npmjs.com/package/@softeneers/db)).
|
|
46
|
-
|
|
46
|
+
**`npm run dev` works immediately even without a database** — the store falls back
|
|
47
|
+
to a pre-seeded in-memory one when MySQL is unreachable. To switch on real
|
|
48
|
+
persistence, set `DB_*` in `.env` and start MySQL:
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
51
|
# #if docker
|
|
50
52
|
docker compose up -d # start MySQL
|
|
51
53
|
# #endif
|
|
54
|
+
# The app auto-creates and seeds tables on first connect; these scripts give
|
|
55
|
+
# you explicit control (e.g. reseeding or a clean reset):
|
|
52
56
|
npm run db:migrate && npm run db:seed
|
|
53
57
|
```
|
|
54
|
-
|
|
55
|
-
Generate with `--no-db` to swap in a dependency-free in-memory store — the CRUD
|
|
56
|
-
API behaves identically.
|
|
57
58
|
# #endif
|
|
58
59
|
# #if auth
|
|
59
60
|
|
|
@@ -67,11 +68,10 @@ mounted at `/api/auth/*`. Set a strong `AUTH_SECRET` in `.env` before deploying.
|
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
npm install
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# #endif
|
|
71
|
+
npm run dev # a pre-seeded CRUD demo — no other setup needed
|
|
72
|
+
```
|
|
73
73
|
# #if db
|
|
74
|
-
|
|
74
|
+
|
|
75
|
+
The app starts immediately on an in-memory demo and uses MySQL automatically once
|
|
76
|
+
it's reachable — see **Database** above to enable persistence.
|
|
75
77
|
# #endif
|
|
76
|
-
npm run dev
|
|
77
|
-
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NewCar } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// Demo garage inventory seeded on first run so `npm run dev` shows a working
|
|
4
|
+
// CRUD demo immediately — into MySQL when a database is reachable, otherwise
|
|
5
|
+
// into the in-memory store.
|
|
6
|
+
export const DEMO_CARS: NewCar[] = [
|
|
7
|
+
{ brand: "Toyota", model: "Corolla", year: 2021 },
|
|
8
|
+
{ brand: "Tesla", model: "Model 3", year: 2023 },
|
|
9
|
+
{ brand: "Ford", model: "Mustang", year: 1969 },
|
|
10
|
+
];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { DEMO_CARS } from "./demo.js";
|
|
1
2
|
import type { Car, NewCar } from "./types.js";
|
|
2
3
|
|
|
3
|
-
// The data layer behind the CRUD routes. With the `db` toggle on
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
4
|
+
// The data layer behind the CRUD routes. With the `db` toggle on it persists to
|
|
5
|
+
// MySQL (Sequelize via @softeneers/db) and **falls back to an in-memory store if
|
|
6
|
+
// the database is unreachable**, so `npm run dev` always yields a working,
|
|
7
|
+
// pre-seeded demo. With `db` off it is always in-memory.
|
|
6
8
|
export interface CarStore {
|
|
7
9
|
list(): Promise<Car[]>;
|
|
8
10
|
get(id: number): Promise<Car | null>;
|
|
@@ -11,8 +13,43 @@ export interface CarStore {
|
|
|
11
13
|
remove(id: number): Promise<boolean>;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
// In-memory backend (the default, and the fallback when no database is reachable).
|
|
17
|
+
function createMemoryStore(seed = true): CarStore {
|
|
18
|
+
let nextId = 1;
|
|
19
|
+
const cars = new Map<number, Car>();
|
|
20
|
+
if (seed) {
|
|
21
|
+
for (const car of DEMO_CARS) {
|
|
22
|
+
const id = nextId++;
|
|
23
|
+
cars.set(id, { id, ...car });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
async list() {
|
|
28
|
+
return [...cars.values()].sort((a, b) => a.id - b.id);
|
|
29
|
+
},
|
|
30
|
+
async get(id) {
|
|
31
|
+
return cars.get(id) ?? null;
|
|
32
|
+
},
|
|
33
|
+
async create(input) {
|
|
34
|
+
const car: Car = { id: nextId++, ...input };
|
|
35
|
+
cars.set(car.id, car);
|
|
36
|
+
return car;
|
|
37
|
+
},
|
|
38
|
+
async update(id, input) {
|
|
39
|
+
const car = cars.get(id);
|
|
40
|
+
if (!car) return null;
|
|
41
|
+
const next: Car = { ...car, ...input };
|
|
42
|
+
cars.set(id, next);
|
|
43
|
+
return next;
|
|
44
|
+
},
|
|
45
|
+
async remove(id) {
|
|
46
|
+
return cars.delete(id);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
// #if db
|
|
15
|
-
import { DataTypes, Model } from "@softeneers/db";
|
|
52
|
+
import { assertConnection, DataTypes, Model } from "@softeneers/db";
|
|
16
53
|
|
|
17
54
|
import { sequelize } from "../db.js";
|
|
18
55
|
|
|
@@ -37,56 +74,77 @@ export { CarModel };
|
|
|
37
74
|
|
|
38
75
|
const toCar = (m: CarModel): Car => ({ id: m.id, brand: m.brand, model: m.model, year: m.year });
|
|
39
76
|
|
|
77
|
+
function createDbStore(): CarStore {
|
|
78
|
+
return {
|
|
79
|
+
async list() {
|
|
80
|
+
return (await CarModel.findAll({ order: [["id", "ASC"]] })).map(toCar);
|
|
81
|
+
},
|
|
82
|
+
async get(id) {
|
|
83
|
+
const m = await CarModel.findByPk(id);
|
|
84
|
+
return m ? toCar(m) : null;
|
|
85
|
+
},
|
|
86
|
+
async create(input) {
|
|
87
|
+
return toCar(await CarModel.create(input));
|
|
88
|
+
},
|
|
89
|
+
async update(id, input) {
|
|
90
|
+
const m = await CarModel.findByPk(id);
|
|
91
|
+
if (!m) return null;
|
|
92
|
+
await m.update(input);
|
|
93
|
+
return toCar(m);
|
|
94
|
+
},
|
|
95
|
+
async remove(id) {
|
|
96
|
+
const m = await CarModel.findByPk(id);
|
|
97
|
+
if (!m) return false;
|
|
98
|
+
await m.destroy();
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resolve the backend once, lazily, on first use: try MySQL (create tables +
|
|
105
|
+
// seed if empty); on any connection error, fall back to the in-memory store.
|
|
106
|
+
let backend: Promise<CarStore> | null = null;
|
|
107
|
+
function resolveBackend(): Promise<CarStore> {
|
|
108
|
+
if (!backend) {
|
|
109
|
+
backend = (async () => {
|
|
110
|
+
try {
|
|
111
|
+
await assertConnection(sequelize);
|
|
112
|
+
await sequelize.sync();
|
|
113
|
+
const db = createDbStore();
|
|
114
|
+
if ((await db.list()).length === 0) {
|
|
115
|
+
for (const car of DEMO_CARS) await db.create(car);
|
|
116
|
+
}
|
|
117
|
+
console.log("Data store: MySQL");
|
|
118
|
+
return db;
|
|
119
|
+
} catch {
|
|
120
|
+
console.warn(
|
|
121
|
+
"Data store: in-memory — database unreachable. Run `docker compose up -d` (and `npm run db:migrate && npm run db:seed`) for MySQL.",
|
|
122
|
+
);
|
|
123
|
+
return createMemoryStore();
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
}
|
|
127
|
+
return backend;
|
|
128
|
+
}
|
|
129
|
+
|
|
40
130
|
export const carStore: CarStore = {
|
|
41
131
|
async list() {
|
|
42
|
-
return (await
|
|
132
|
+
return (await resolveBackend()).list();
|
|
43
133
|
},
|
|
44
134
|
async get(id) {
|
|
45
|
-
|
|
46
|
-
return m ? toCar(m) : null;
|
|
135
|
+
return (await resolveBackend()).get(id);
|
|
47
136
|
},
|
|
48
137
|
async create(input) {
|
|
49
|
-
return
|
|
138
|
+
return (await resolveBackend()).create(input);
|
|
50
139
|
},
|
|
51
140
|
async update(id, input) {
|
|
52
|
-
|
|
53
|
-
if (!m) return null;
|
|
54
|
-
await m.update(input);
|
|
55
|
-
return toCar(m);
|
|
141
|
+
return (await resolveBackend()).update(id, input);
|
|
56
142
|
},
|
|
57
143
|
async remove(id) {
|
|
58
|
-
|
|
59
|
-
if (!m) return false;
|
|
60
|
-
await m.destroy();
|
|
61
|
-
return true;
|
|
144
|
+
return (await resolveBackend()).remove(id);
|
|
62
145
|
},
|
|
63
146
|
};
|
|
64
147
|
// #endif
|
|
65
148
|
// #if !db
|
|
66
|
-
|
|
67
|
-
const cars = new Map<number, Car>();
|
|
68
|
-
|
|
69
|
-
export const carStore: CarStore = {
|
|
70
|
-
async list() {
|
|
71
|
-
return [...cars.values()].sort((a, b) => a.id - b.id);
|
|
72
|
-
},
|
|
73
|
-
async get(id) {
|
|
74
|
-
return cars.get(id) ?? null;
|
|
75
|
-
},
|
|
76
|
-
async create(input) {
|
|
77
|
-
const car: Car = { id: nextId++, ...input };
|
|
78
|
-
cars.set(car.id, car);
|
|
79
|
-
return car;
|
|
80
|
-
},
|
|
81
|
-
async update(id, input) {
|
|
82
|
-
const car = cars.get(id);
|
|
83
|
-
if (!car) return null;
|
|
84
|
-
const next: Car = { ...car, ...input };
|
|
85
|
-
cars.set(id, next);
|
|
86
|
-
return next;
|
|
87
|
-
},
|
|
88
|
-
async remove(id) {
|
|
89
|
-
return cars.delete(id);
|
|
90
|
-
},
|
|
91
|
-
};
|
|
149
|
+
export const carStore: CarStore = createMemoryStore();
|
|
92
150
|
// #endif
|
|
@@ -4,12 +4,6 @@ import { cors } from "hono/cors";
|
|
|
4
4
|
|
|
5
5
|
import { cars } from "./cars/routes.js";
|
|
6
6
|
import { env } from "./env.js";
|
|
7
|
-
// #if db
|
|
8
|
-
import { assertConnection } from "@softeneers/db";
|
|
9
|
-
|
|
10
|
-
import { sequelize } from "./db.js";
|
|
11
|
-
import "./cars/store.js"; // registers the Car model on the connection
|
|
12
|
-
// #endif
|
|
13
7
|
// #if auth
|
|
14
8
|
import { auth } from "./auth/auth.js";
|
|
15
9
|
// #endif
|
|
@@ -27,18 +21,6 @@ app.route("/api/cars", cars);
|
|
|
27
21
|
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));
|
|
28
22
|
// #endif
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await assertConnection(sequelize);
|
|
33
|
-
await sequelize.sync();
|
|
34
|
-
console.log("Database connected.");
|
|
35
|
-
// #endif
|
|
36
|
-
serve({ fetch: app.fetch, port: env.PORT }, (info) => {
|
|
37
|
-
console.log(`API running on http://localhost:${info.port}`);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
start().catch((error) => {
|
|
42
|
-
console.error("Failed to start:", error);
|
|
43
|
-
process.exit(1);
|
|
24
|
+
serve({ fetch: app.fetch, port: env.PORT }, (info) => {
|
|
25
|
+
console.log(`API running on http://localhost:${info.port}`);
|
|
44
26
|
});
|
|
@@ -2,24 +2,19 @@ import "dotenv/config";
|
|
|
2
2
|
|
|
3
3
|
import { assertConnection } from "@softeneers/db";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { DEMO_CARS } from "../cars/demo.js";
|
|
6
|
+
import { CarModel } from "../cars/store.js";
|
|
6
7
|
import { sequelize } from "../db.js";
|
|
7
8
|
|
|
8
9
|
await assertConnection(sequelize);
|
|
9
10
|
await sequelize.sync();
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
-
console.log(`Skipped seeding — ${
|
|
12
|
+
const count = await CarModel.count();
|
|
13
|
+
if (count > 0) {
|
|
14
|
+
console.log(`Skipped seeding — ${count} cars already present.`);
|
|
14
15
|
} else {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ brand: "Tesla", model: "Model 3", year: 2023 },
|
|
18
|
-
{ brand: "Ford", model: "Mustang", year: 1969 },
|
|
19
|
-
]) {
|
|
20
|
-
await carStore.create(car);
|
|
21
|
-
}
|
|
22
|
-
console.log("Seeded 3 cars.");
|
|
16
|
+
await CarModel.bulkCreate([...DEMO_CARS]);
|
|
17
|
+
console.log(`Seeded ${DEMO_CARS.length} cars.`);
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
await sequelize.close();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
/.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
|
|
27
|
+
# debug
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
yarn-error.log*
|
|
31
|
+
.pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# env files (can opt-in for committing if needed)
|
|
34
|
+
.env*
|
|
35
|
+
|
|
36
|
+
# vercel
|
|
37
|
+
.vercel
|
|
38
|
+
|
|
39
|
+
# typescript
|
|
40
|
+
*.tsbuildinfo
|
|
41
|
+
next-env.d.ts
|
|
@@ -39,16 +39,18 @@ functions — no hand-written API client, end-to-end type safety.
|
|
|
39
39
|
## Database
|
|
40
40
|
|
|
41
41
|
Persistence is MySQL via Sequelize ([`@softeneers/db`](https://www.npmjs.com/package/@softeneers/db)).
|
|
42
|
-
|
|
42
|
+
**`npm run dev` works immediately even without a database** — the server falls
|
|
43
|
+
back to a pre-seeded in-memory store when MySQL is unreachable. To switch on real
|
|
44
|
+
persistence, set `DB_*` in `.env` and start MySQL:
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
47
|
# #if docker
|
|
46
48
|
docker compose up -d # start MySQL
|
|
47
49
|
# #endif
|
|
50
|
+
# The server auto-creates and seeds tables on first connect; these scripts give
|
|
51
|
+
# you explicit control:
|
|
48
52
|
npm run db:migrate && npm run db:seed
|
|
49
53
|
```
|
|
50
|
-
|
|
51
|
-
Generate with `--no-db` to use a dependency-free in-memory store instead.
|
|
52
54
|
# #endif
|
|
53
55
|
# #if auth
|
|
54
56
|
|
|
@@ -63,11 +65,10 @@ served at `/api/auth/*`. Set a strong `AUTH_SECRET` in `.env`, and add the
|
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
npm install
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# #endif
|
|
68
|
+
npm run dev # a pre-seeded CRUD demo on http://localhost:3000 — no other setup
|
|
69
|
+
```
|
|
69
70
|
# #if db
|
|
70
|
-
|
|
71
|
+
|
|
72
|
+
The app starts immediately on an in-memory demo and uses MySQL automatically once
|
|
73
|
+
it's reachable — see **Database** above to enable persistence.
|
|
71
74
|
# #endif
|
|
72
|
-
npm run dev
|
|
73
|
-
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NewCar } from '../cars/types'
|
|
2
|
+
|
|
3
|
+
// Demo garage inventory seeded on first run so `npm run dev` shows a working
|
|
4
|
+
// CRUD demo immediately — into MySQL when reachable, else the in-memory store.
|
|
5
|
+
export const DEMO_CARS: NewCar[] = [
|
|
6
|
+
{ brand: 'Toyota', model: 'Corolla', year: 2021 },
|
|
7
|
+
{ brand: 'Tesla', model: 'Model 3', year: 2023 },
|
|
8
|
+
{ brand: 'Ford', model: 'Mustang', year: 1969 },
|
|
9
|
+
]
|
|
@@ -2,24 +2,19 @@ import 'dotenv/config'
|
|
|
2
2
|
|
|
3
3
|
import { assertConnection } from '@softeneers/db'
|
|
4
4
|
|
|
5
|
+
import { DEMO_CARS } from '../demo'
|
|
5
6
|
import { sequelize } from '../db'
|
|
6
|
-
import {
|
|
7
|
+
import { CarModel } from '../store'
|
|
7
8
|
|
|
8
9
|
await assertConnection(sequelize)
|
|
9
10
|
await sequelize.sync()
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
13
|
-
console.log(`Skipped seeding — ${
|
|
12
|
+
const count = await CarModel.count()
|
|
13
|
+
if (count > 0) {
|
|
14
|
+
console.log(`Skipped seeding — ${count} cars already present.`)
|
|
14
15
|
} else {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ brand: 'Tesla', model: 'Model 3', year: 2023 },
|
|
18
|
-
{ brand: 'Ford', model: 'Mustang', year: 1969 },
|
|
19
|
-
]) {
|
|
20
|
-
await carStore.create(car)
|
|
21
|
-
}
|
|
22
|
-
console.log('Seeded 3 cars.')
|
|
16
|
+
await CarModel.bulkCreate([...DEMO_CARS])
|
|
17
|
+
console.log(`Seeded ${DEMO_CARS.length} cars.`)
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
await sequelize.close()
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Car, NewCar } from '../cars/types'
|
|
2
|
+
import { DEMO_CARS } from './demo'
|
|
2
3
|
|
|
3
|
-
// Server-only data layer behind the cars server functions. With `db` on it
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Server-only data layer behind the cars server functions. With `db` on it
|
|
5
|
+
// persists to MySQL (Sequelize via @softeneers/db) and **falls back to an
|
|
6
|
+
// in-memory store if the database is unreachable**, so `npm run dev` always
|
|
7
|
+
// yields a working, pre-seeded demo. With `db` off it is always in-memory.
|
|
8
|
+
// Never bundled into the client — only reached from server functions.
|
|
7
9
|
export interface CarStore {
|
|
8
10
|
list(): Promise<Array<Car>>
|
|
9
11
|
get(id: number): Promise<Car | null>
|
|
@@ -11,8 +13,34 @@ export interface CarStore {
|
|
|
11
13
|
remove(id: number): Promise<boolean>
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
// In-memory backend (the default, and the fallback when no database is reachable).
|
|
17
|
+
function createMemoryStore(): CarStore {
|
|
18
|
+
let nextId = 1
|
|
19
|
+
const cars = new Map<number, Car>()
|
|
20
|
+
for (const car of DEMO_CARS) {
|
|
21
|
+
const id = nextId++
|
|
22
|
+
cars.set(id, { id, ...car })
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
async list() {
|
|
26
|
+
return [...cars.values()].sort((a, b) => a.id - b.id)
|
|
27
|
+
},
|
|
28
|
+
async get(id) {
|
|
29
|
+
return cars.get(id) ?? null
|
|
30
|
+
},
|
|
31
|
+
async create(input) {
|
|
32
|
+
const car: Car = { id: nextId++, ...input }
|
|
33
|
+
cars.set(car.id, car)
|
|
34
|
+
return car
|
|
35
|
+
},
|
|
36
|
+
async remove(id) {
|
|
37
|
+
return cars.delete(id)
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
// #if db
|
|
15
|
-
import { DataTypes, Model } from '@softeneers/db'
|
|
43
|
+
import { DataTypes, Model, assertConnection } from '@softeneers/db'
|
|
16
44
|
|
|
17
45
|
import { sequelize } from './db'
|
|
18
46
|
|
|
@@ -37,43 +65,68 @@ export { CarModel }
|
|
|
37
65
|
|
|
38
66
|
const toCar = (m: CarModel): Car => ({ id: m.id, brand: m.brand, model: m.model, year: m.year })
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
function createDbStore(): CarStore {
|
|
69
|
+
return {
|
|
70
|
+
async list() {
|
|
71
|
+
return (await CarModel.findAll({ order: [['id', 'ASC']] })).map(toCar)
|
|
72
|
+
},
|
|
73
|
+
async get(id) {
|
|
74
|
+
const m = await CarModel.findByPk(id)
|
|
75
|
+
return m ? toCar(m) : null
|
|
76
|
+
},
|
|
77
|
+
async create(input) {
|
|
78
|
+
return toCar(await CarModel.create(input))
|
|
79
|
+
},
|
|
80
|
+
async remove(id) {
|
|
81
|
+
const m = await CarModel.findByPk(id)
|
|
82
|
+
if (!m) return false
|
|
83
|
+
await m.destroy()
|
|
84
|
+
return true
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Resolve the backend once, lazily, on first use: try MySQL (create tables +
|
|
90
|
+
// seed if empty); on any connection error, fall back to the in-memory store.
|
|
91
|
+
let backend: Promise<CarStore> | null = null
|
|
92
|
+
function resolveBackend(): Promise<CarStore> {
|
|
93
|
+
if (!backend) {
|
|
94
|
+
backend = (async () => {
|
|
95
|
+
try {
|
|
96
|
+
await assertConnection(sequelize)
|
|
97
|
+
await sequelize.sync()
|
|
98
|
+
const db = createDbStore()
|
|
99
|
+
if ((await db.list()).length === 0) {
|
|
100
|
+
for (const car of DEMO_CARS) await db.create(car)
|
|
101
|
+
}
|
|
102
|
+
console.log('Data store: MySQL')
|
|
103
|
+
return db
|
|
104
|
+
} catch {
|
|
105
|
+
console.warn(
|
|
106
|
+
'Data store: in-memory — database unreachable. Run `docker compose up -d` (and `npm run db:migrate && npm run db:seed`) for MySQL.',
|
|
107
|
+
)
|
|
108
|
+
return createMemoryStore()
|
|
109
|
+
}
|
|
110
|
+
})()
|
|
111
|
+
}
|
|
112
|
+
return backend
|
|
57
113
|
}
|
|
58
|
-
// #endif
|
|
59
|
-
// #if !db
|
|
60
|
-
let nextId = 1
|
|
61
|
-
const cars = new Map<number, Car>()
|
|
62
114
|
|
|
63
115
|
export const carStore: CarStore = {
|
|
64
116
|
async list() {
|
|
65
|
-
return
|
|
117
|
+
return (await resolveBackend()).list()
|
|
66
118
|
},
|
|
67
119
|
async get(id) {
|
|
68
|
-
return
|
|
120
|
+
return (await resolveBackend()).get(id)
|
|
69
121
|
},
|
|
70
122
|
async create(input) {
|
|
71
|
-
|
|
72
|
-
cars.set(car.id, car)
|
|
73
|
-
return car
|
|
123
|
+
return (await resolveBackend()).create(input)
|
|
74
124
|
},
|
|
75
125
|
async remove(id) {
|
|
76
|
-
return
|
|
126
|
+
return (await resolveBackend()).remove(id)
|
|
77
127
|
},
|
|
78
128
|
}
|
|
79
129
|
// #endif
|
|
130
|
+
// #if !db
|
|
131
|
+
export const carStore: CarStore = createMemoryStore()
|
|
132
|
+
// #endif
|