@cavulsqa/create 0.1.2 → 2.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.
Files changed (42) hide show
  1. package/README.md +28 -10
  2. package/bin/create.mjs +44 -1
  3. package/lib/scaffold.mjs +15 -1
  4. package/package.json +2 -2
  5. package/templates/f7-app/.claude/rules/database.md +39 -0
  6. package/templates/f7-app/.env.example +23 -0
  7. package/templates/f7-app/CLAUDE.md +8 -3
  8. package/templates/f7-app/auto-imports.d.ts +7 -0
  9. package/templates/f7-app/components.d.ts +2 -0
  10. package/templates/f7-app/package.json +6 -6
  11. package/templates/f7-app/src/app/pragmas.config.ts +51 -0
  12. package/templates/f7-app/src/app/storage.config.ts +56 -0
  13. package/templates/f7-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
  14. package/templates/f7-app/src/domains/benchmark/benchmark.suite.ts +662 -0
  15. package/templates/f7-app/src/domains/sales/sales.repository.ts +1 -1
  16. package/templates/f7-app/src/env.d.ts +15 -0
  17. package/templates/f7-app/src/locales/en.json +37 -2
  18. package/templates/f7-app/src/locales/fr.json +37 -2
  19. package/templates/f7-app/src/main.ts +6 -16
  20. package/templates/f7-app/src/modules/demo/components/DemoBenchmark.vue +112 -0
  21. package/templates/f7-app/src/modules/demo/components/DemoPipelineBenchmark.vue +10 -0
  22. package/templates/f7-app/src/modules/demo/composables/useBenchmark.ts +139 -0
  23. package/templates/f7-app/src/modules/demo/composables/useReactiveDemo.ts +5 -1
  24. package/templates/f7-app/src/modules/demo/views/DemoView.vue +9 -2
  25. package/templates/f7-app/src/modules/settings/views/SettingsView.vue +31 -0
  26. package/templates/f7-app/src/plugins/bootstrapError.ts +60 -0
  27. package/templates/f7-app/src/shared/database/candidates/index.ts +3 -0
  28. package/templates/f7-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
  29. package/templates/f7-app/src/shared/database/candidates/types.ts +41 -0
  30. package/templates/f7-app/src/shared/database/candidates/waSqlite.ts +58 -0
  31. package/templates/f7-app/src/shared/database/database.ts +151 -30
  32. package/templates/f7-app/src/shared/database/migrations.ts +143 -1
  33. package/templates/f7-app/src/shared/database/opfs.worker.ts +5 -0
  34. package/templates/f7-app/src/shared/database/schema.ts +94 -0
  35. package/templates/f7-app/src/shared/database/storage.ts +94 -0
  36. package/templates/f7-app/src/shared/database/wa.worker.ts +5 -0
  37. package/templates/f7-app/src/shared/utils/resolvers/resolvers.ts +1 -2
  38. package/templates/f7-app/tests/benchmark.suite.test.ts +104 -0
  39. package/templates/f7-app/tests/migrations.test.ts +74 -0
  40. package/templates/f7-app/tests/storage.test.ts +117 -0
  41. package/templates/f7-app/vite.config.ts +3 -1
  42. package/templates/f7-app/tsconfig.node.json +0 -14
@@ -0,0 +1,209 @@
1
+ import { sql, type Insertable, type Kysely } from "kysely";
2
+ import type { Database } from "@/shared/database/schema";
3
+
4
+ /**
5
+ * Roughly 100k rows across ten tables, with the cardinality spread a real domain has: ten regions,
6
+ * five thousand customers, forty thousand order lines. A join across five of these touches a 10-row
7
+ * table and a 40k-row table in one query, which is where index choice starts to show.
8
+ */
9
+ export const SCALE = {
10
+ regions: 10,
11
+ cities: 200,
12
+ customers: 5_000,
13
+ categories: 50,
14
+ products: 2_000,
15
+ warehouses: 20,
16
+ /** warehouses x a slice of products, the biggest table here. */
17
+ stock: 40_000,
18
+ orders: 10_000,
19
+ /** ~4 lines per order. */
20
+ orderLines: 40_000,
21
+ payments: 8_000,
22
+ } as const;
23
+
24
+ export const TOTAL_ROWS = Object.values(SCALE).reduce((sum, n) => sum + n, 0);
25
+
26
+ /**
27
+ * SQLite caps the parameters in one statement - 999 on older builds - so a multi-row insert is
28
+ * chunked by parameter budget rather than by row count. One statement per chunk instead of one per
29
+ * row turns a 75-second seed into a few seconds; at 0.7 ms per single-row insert, 100k rows one at a
30
+ * time would take over a minute of pure commit.
31
+ */
32
+ const PARAMETER_BUDGET = 900;
33
+
34
+ async function insertMany<T extends keyof Database>(
35
+ db: Kysely<Database>,
36
+ table: T & string,
37
+ rows: Insertable<Database[T]>[],
38
+ ): Promise<void> {
39
+ if (!rows.length) return;
40
+ const columns = Object.keys(rows[0] as object).length;
41
+ const chunk = Math.max(1, Math.floor(PARAMETER_BUDGET / Math.max(columns, 1)));
42
+
43
+ for (let index = 0; index < rows.length; index += chunk) {
44
+ await db
45
+ .insertInto(table)
46
+ .values(rows.slice(index, index + chunk) as never)
47
+ .execute();
48
+ }
49
+ }
50
+
51
+ const NAMES = ["Alger", "Oran", "Blida", "Setif", "Annaba", "Batna", "Djelfa", "Biskra"];
52
+ const METHODS = ["cash", "cheque", "transfer", "credit"];
53
+ const STATUSES = ["draft", "confirmed", "delivered", "cancelled"];
54
+
55
+ /** Deterministic, so two runs seed the same data and a slow query is slow for the same reason. */
56
+ function pseudo(seed: number, modulo: number): number {
57
+ return ((seed * 1103515245 + 12345) >>> 8) % modulo;
58
+ }
59
+
60
+ export async function datasetRowCount(db: Kysely<Database>): Promise<number> {
61
+ const row = await db
62
+ .selectNoFrom((eb) => [
63
+ eb
64
+ .selectFrom("bench_order_line")
65
+ .select((e) => e.fn.countAll<number>().as("n"))
66
+ .as("lines"),
67
+ eb
68
+ .selectFrom("bench_stock")
69
+ .select((e) => e.fn.countAll<number>().as("n"))
70
+ .as("stock"),
71
+ ])
72
+ .executeTakeFirst();
73
+ return Number(row?.lines ?? 0) + Number(row?.stock ?? 0);
74
+ }
75
+
76
+ /**
77
+ * Seeds once and stays. Re-seeding on every run would dominate the measurement, and a dataset that
78
+ * changes between runs makes two runs incomparable - so this is skipped when the row counts already
79
+ * match, and the write cases below are careful to leave the dataset as they found it.
80
+ */
81
+ export async function ensureDataset(
82
+ db: Kysely<Database>,
83
+ onProgress?: (what: string) => void,
84
+ ): Promise<{ seeded: boolean; rows: number }> {
85
+ const existing = await datasetRowCount(db);
86
+ if (existing >= SCALE.orderLines + SCALE.stock) return { seeded: false, rows: existing };
87
+
88
+ const at = new Date(0).toISOString();
89
+ const step = (what: string) => onProgress?.(`seeding ${what}`);
90
+
91
+ // Cleared in dependency order; a partial previous seed must not survive.
92
+ step("reset");
93
+ for (const table of [
94
+ "bench_payment",
95
+ "bench_order_line",
96
+ "bench_order",
97
+ "bench_stock",
98
+ "bench_product",
99
+ "bench_category",
100
+ "bench_customer",
101
+ "bench_city",
102
+ "bench_region",
103
+ "bench_warehouse",
104
+ ] as const) {
105
+ await sql.raw(`DELETE FROM ${table}`).execute(db);
106
+ }
107
+
108
+ step("regions and cities");
109
+ await insertMany(
110
+ db,
111
+ "bench_region",
112
+ Array.from({ length: SCALE.regions }, (_, i) => ({ name: `Region ${String(i + 1)}` })),
113
+ );
114
+ await insertMany(
115
+ db,
116
+ "bench_city",
117
+ Array.from({ length: SCALE.cities }, (_, i) => ({
118
+ region_id: (i % SCALE.regions) + 1,
119
+ name: `${NAMES[i % NAMES.length]} ${String(i + 1)}`,
120
+ })),
121
+ );
122
+
123
+ step("customers");
124
+ await insertMany(
125
+ db,
126
+ "bench_customer",
127
+ Array.from({ length: SCALE.customers }, (_, i) => ({
128
+ city_id: (i % SCALE.cities) + 1,
129
+ code: `C${String(i + 1).padStart(6, "0")}`,
130
+ name: `Customer ${String(i + 1)}`,
131
+ // Long enough that a LIKE scan over 5k rows is real work, and varied so it matches sparsely.
132
+ notes: `route ${String(pseudo(i, 40))} sector ${String(pseudo(i + 7, 12))} ${NAMES[pseudo(i, NAMES.length)]} follow-up pending review`,
133
+ credit_cents: pseudo(i, 500_000),
134
+ created_at: at,
135
+ })),
136
+ );
137
+
138
+ step("catalogue");
139
+ await insertMany(
140
+ db,
141
+ "bench_category",
142
+ Array.from({ length: SCALE.categories }, (_, i) => ({ name: `Category ${String(i + 1)}` })),
143
+ );
144
+ await insertMany(
145
+ db,
146
+ "bench_product",
147
+ Array.from({ length: SCALE.products }, (_, i) => ({
148
+ category_id: (i % SCALE.categories) + 1,
149
+ sku: `SKU${String(i + 1).padStart(6, "0")}`,
150
+ name: `Product ${String(i + 1)}`,
151
+ price_cents: 500 + pseudo(i, 200_000),
152
+ })),
153
+ );
154
+
155
+ step("warehouses and stock");
156
+ await insertMany(
157
+ db,
158
+ "bench_warehouse",
159
+ Array.from({ length: SCALE.warehouses }, (_, i) => ({ name: `Warehouse ${String(i + 1)}` })),
160
+ );
161
+ await insertMany(
162
+ db,
163
+ "bench_stock",
164
+ Array.from({ length: SCALE.stock }, (_, i) => ({
165
+ warehouse_id: (i % SCALE.warehouses) + 1,
166
+ product_id: (i % SCALE.products) + 1,
167
+ quantity: pseudo(i, 900),
168
+ })),
169
+ );
170
+
171
+ step("orders");
172
+ await insertMany(
173
+ db,
174
+ "bench_order",
175
+ Array.from({ length: SCALE.orders }, (_, i) => ({
176
+ customer_id: (i % SCALE.customers) + 1,
177
+ reference: `SO-${String(i + 1).padStart(7, "0")}`,
178
+ status: STATUSES[pseudo(i, STATUSES.length)]!,
179
+ created_at: at,
180
+ total_cents: pseudo(i, 400_000),
181
+ })),
182
+ );
183
+
184
+ step("order lines");
185
+ await insertMany(
186
+ db,
187
+ "bench_order_line",
188
+ Array.from({ length: SCALE.orderLines }, (_, i) => ({
189
+ order_id: (i % SCALE.orders) + 1,
190
+ product_id: pseudo(i, SCALE.products) + 1,
191
+ quantity: 1 + pseudo(i, 12),
192
+ unit_price_cents: 500 + pseudo(i + 3, 200_000),
193
+ })),
194
+ );
195
+
196
+ step("payments");
197
+ await insertMany(
198
+ db,
199
+ "bench_payment",
200
+ Array.from({ length: SCALE.payments }, (_, i) => ({
201
+ order_id: (i % SCALE.orders) + 1,
202
+ amount_cents: pseudo(i, 300_000),
203
+ method: METHODS[pseudo(i, METHODS.length)]!,
204
+ created_at: at,
205
+ })),
206
+ );
207
+
208
+ return { seeded: true, rows: await datasetRowCount(db) };
209
+ }