create-softeneers-app 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-softeneers-app",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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
- Configure `DB_*` in `.env`, then:
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
- # #if docker
71
- docker compose up -d
72
- # #endif
71
+ npm run dev # a pre-seeded CRUD demo — no other setup needed
72
+ ```
73
73
  # #if db
74
- npm run db:migrate && npm run db:seed
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
- ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "toggles": { "db": true, "auth": false, "docker": true },
2
+ "toggles": { "db": false, "auth": false, "docker": false },
3
3
  "fragments": {
4
4
  "db": {
5
5
  "removePaths": ["src/db.ts", "src/scripts", "docker-compose.yml"],
@@ -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 this is backed
4
- // by MySQL (Sequelize via @softeneers/db); with it off it's an in-memory Map so
5
- // the API runs with zero infrastructure. Both satisfy the same CarStore.
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 CarModel.findAll({ order: [["id", "ASC"]] })).map(toCar);
132
+ return (await resolveBackend()).list();
43
133
  },
44
134
  async get(id) {
45
- const m = await CarModel.findByPk(id);
46
- return m ? toCar(m) : null;
135
+ return (await resolveBackend()).get(id);
47
136
  },
48
137
  async create(input) {
49
- return toCar(await CarModel.create(input));
138
+ return (await resolveBackend()).create(input);
50
139
  },
51
140
  async update(id, input) {
52
- const m = await CarModel.findByPk(id);
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
- const m = await CarModel.findByPk(id);
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
- let nextId = 1;
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
- async function start(): Promise<void> {
42
- // #if db
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 { carStore } from "../cars/store.js";
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 existing = await carStore.list();
12
- if (existing.length > 0) {
13
- console.log(`Skipped seeding — ${existing.length} cars already present.`);
12
+ const count = await CarModel.count();
13
+ if (count > 0) {
14
+ console.log(`Skipped seeding — ${count} cars already present.`);
14
15
  } else {
15
- for (const car of [
16
- { brand: "Toyota", model: "Corolla", year: 2021 },
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
- Configure `DB_*` in `.env`, then:
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
- # #if docker
71
- docker compose up -d
72
- # #endif
71
+ npm run dev # a pre-seeded CRUD demo — no other setup needed
72
+ ```
73
73
  # #if db
74
- npm run db:migrate && npm run db:seed
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
- ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "toggles": { "db": true, "auth": false, "docker": true },
2
+ "toggles": { "db": false, "auth": false, "docker": false },
3
3
  "fragments": {
4
4
  "db": {
5
5
  "removePaths": ["src/db.ts", "src/scripts", "docker-compose.yml"],
@@ -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 this is backed
4
- // by MySQL (Sequelize via @softeneers/db); with it off it's an in-memory Map so
5
- // the API runs with zero infrastructure. Both satisfy the same CarStore.
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 CarModel.findAll({ order: [["id", "ASC"]] })).map(toCar);
132
+ return (await resolveBackend()).list();
43
133
  },
44
134
  async get(id) {
45
- const m = await CarModel.findByPk(id);
46
- return m ? toCar(m) : null;
135
+ return (await resolveBackend()).get(id);
47
136
  },
48
137
  async create(input) {
49
- return toCar(await CarModel.create(input));
138
+ return (await resolveBackend()).create(input);
50
139
  },
51
140
  async update(id, input) {
52
- const m = await CarModel.findByPk(id);
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
- const m = await CarModel.findByPk(id);
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
- let nextId = 1;
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
- async function start(): Promise<void> {
31
- // #if db
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 { carStore } from "../cars/store.js";
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 existing = await carStore.list();
12
- if (existing.length > 0) {
13
- console.log(`Skipped seeding — ${existing.length} cars already present.`);
12
+ const count = await CarModel.count();
13
+ if (count > 0) {
14
+ console.log(`Skipped seeding — ${count} cars already present.`);
14
15
  } else {
15
- for (const car of [
16
- { brand: "Toyota", model: "Corolla", year: 2021 },
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();
@@ -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
- Set `DB_*` in `.env`, then:
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
- # #if docker
67
- docker compose up -d
68
- # #endif
68
+ npm run dev # a pre-seeded CRUD demo on http://localhost:3000 — no other setup
69
+ ```
69
70
  # #if db
70
- npm run db:migrate && npm run db:seed
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
- ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "toggles": { "db": true, "auth": false, "docker": true },
2
+ "toggles": { "db": false, "auth": false, "docker": false },
3
3
  "fragments": {
4
4
  "db": {
5
5
  "removePaths": ["src/server/db.ts", "src/server/scripts", "docker-compose.yml"],
@@ -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 { carStore } from '../store'
7
+ import { CarModel } from '../store'
7
8
 
8
9
  await assertConnection(sequelize)
9
10
  await sequelize.sync()
10
11
 
11
- const existing = await carStore.list()
12
- if (existing.length > 0) {
13
- console.log(`Skipped seeding — ${existing.length} cars already present.`)
12
+ const count = await CarModel.count()
13
+ if (count > 0) {
14
+ console.log(`Skipped seeding — ${count} cars already present.`)
14
15
  } else {
15
- for (const car of [
16
- { brand: 'Toyota', model: 'Corolla', year: 2021 },
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 is
4
- // backed by MySQL (Sequelize via @softeneers/db); with it off it is an in-memory
5
- // Map. Both satisfy the same CarStore. This module is never bundled into the
6
- // client it is only reached from server functions.
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
- export const carStore: CarStore = {
41
- async list() {
42
- return (await CarModel.findAll({ order: [['id', 'ASC']] })).map(toCar)
43
- },
44
- async get(id) {
45
- const m = await CarModel.findByPk(id)
46
- return m ? toCar(m) : null
47
- },
48
- async create(input) {
49
- return toCar(await CarModel.create(input))
50
- },
51
- async remove(id) {
52
- const m = await CarModel.findByPk(id)
53
- if (!m) return false
54
- await m.destroy()
55
- return true
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 [...cars.values()].sort((a, b) => a.id - b.id)
117
+ return (await resolveBackend()).list()
66
118
  },
67
119
  async get(id) {
68
- return cars.get(id) ?? null
120
+ return (await resolveBackend()).get(id)
69
121
  },
70
122
  async create(input) {
71
- const car: Car = { id: nextId++, ...input }
72
- cars.set(car.id, car)
73
- return car
123
+ return (await resolveBackend()).create(input)
74
124
  },
75
125
  async remove(id) {
76
- return cars.delete(id)
126
+ return (await resolveBackend()).remove(id)
77
127
  },
78
128
  }
79
129
  // #endif
130
+ // #if !db
131
+ export const carStore: CarStore = createMemoryStore()
132
+ // #endif