fossyl 0.9.0 → 0.11.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 +5 -0
- package/dist/index.js +246 -23
- package/dist/index.mjs +246 -23
- package/package.json +10 -2
package/README.md
ADDED
package/dist/index.js
CHANGED
|
@@ -76,7 +76,30 @@ async function promptForOptions(projectName) {
|
|
|
76
76
|
p.cancel("Operation cancelled.");
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
let dialect;
|
|
80
|
+
if (database === "kysely") {
|
|
81
|
+
dialect = await p.select({
|
|
82
|
+
message: "Database dialect:",
|
|
83
|
+
options: [
|
|
84
|
+
{ value: "sqlite", label: "SQLite", hint: "recommended - great for per-customer databases" },
|
|
85
|
+
{ value: "postgres", label: "PostgreSQL" },
|
|
86
|
+
{ value: "mysql", label: "MySQL" }
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
if (p.isCancel(dialect)) {
|
|
90
|
+
p.cancel("Operation cancelled.");
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const docker = await p.confirm({
|
|
95
|
+
message: "Include Docker setup?",
|
|
96
|
+
initialValue: true
|
|
97
|
+
});
|
|
98
|
+
if (p.isCancel(docker)) {
|
|
99
|
+
p.cancel("Operation cancelled.");
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return { name, server, validator, database, dialect, docker };
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
// src/scaffold.ts
|
|
@@ -105,8 +128,15 @@ function generatePackageJson(options) {
|
|
|
105
128
|
if (options.database === "kysely") {
|
|
106
129
|
dependencies["@fossyl/kysely"] = "^0.9.0";
|
|
107
130
|
dependencies["kysely"] = "^0.27.0";
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
if (options.dialect === "sqlite") {
|
|
132
|
+
dependencies["better-sqlite3"] = "^11.0.0";
|
|
133
|
+
devDependencies["@types/better-sqlite3"] = "^7.6.0";
|
|
134
|
+
} else if (options.dialect === "mysql") {
|
|
135
|
+
dependencies["mysql2"] = "^3.11.0";
|
|
136
|
+
} else {
|
|
137
|
+
dependencies["pg"] = "^8.13.0";
|
|
138
|
+
devDependencies["@types/pg"] = "^8.11.0";
|
|
139
|
+
}
|
|
110
140
|
}
|
|
111
141
|
const pkg = {
|
|
112
142
|
name: options.name === "." ? "my-fossyl-api" : options.name,
|
|
@@ -147,8 +177,17 @@ PORT=3000
|
|
|
147
177
|
if (options.database === "kysely") {
|
|
148
178
|
content += `
|
|
149
179
|
# Database
|
|
150
|
-
DATABASE_URL=postgres://user:password@localhost:5432/mydb
|
|
151
180
|
`;
|
|
181
|
+
if (options.dialect === "sqlite") {
|
|
182
|
+
content += `DATABASE_PATH=./data/app.db
|
|
183
|
+
`;
|
|
184
|
+
} else if (options.dialect === "mysql") {
|
|
185
|
+
content += `DATABASE_URL=mysql://user:password@localhost:3306/mydb
|
|
186
|
+
`;
|
|
187
|
+
} else {
|
|
188
|
+
content += `DATABASE_URL=postgres://user:password@localhost:5432/mydb
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
152
191
|
}
|
|
153
192
|
return content;
|
|
154
193
|
}
|
|
@@ -394,7 +433,39 @@ export function startServer(routes: Route[], port: number): void {
|
|
|
394
433
|
}
|
|
395
434
|
|
|
396
435
|
// src/templates/database/kysely.ts
|
|
397
|
-
function generateKyselySetup() {
|
|
436
|
+
function generateKyselySetup(dialect = "postgres") {
|
|
437
|
+
if (dialect === "sqlite") {
|
|
438
|
+
return `import Database from 'better-sqlite3';
|
|
439
|
+
import { Kysely, SqliteDialect } from 'kysely';
|
|
440
|
+
import type { DB } from './types/db';
|
|
441
|
+
|
|
442
|
+
const databasePath = process.env.DATABASE_PATH || './data/app.db';
|
|
443
|
+
|
|
444
|
+
export const db = new Kysely<DB>({
|
|
445
|
+
dialect: new SqliteDialect({
|
|
446
|
+
database: new Database(databasePath),
|
|
447
|
+
}),
|
|
448
|
+
});
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
if (dialect === "mysql") {
|
|
452
|
+
return `import { createPool } from 'mysql2';
|
|
453
|
+
import { Kysely, MysqlDialect } from 'kysely';
|
|
454
|
+
import type { DB } from './types/db';
|
|
455
|
+
|
|
456
|
+
const connectionString = process.env.DATABASE_URL;
|
|
457
|
+
|
|
458
|
+
if (!connectionString) {
|
|
459
|
+
throw new Error('DATABASE_URL environment variable is required');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export const db = new Kysely<DB>({
|
|
463
|
+
dialect: new MysqlDialect({
|
|
464
|
+
pool: createPool(connectionString),
|
|
465
|
+
}),
|
|
466
|
+
});
|
|
467
|
+
`;
|
|
468
|
+
}
|
|
398
469
|
return `import { Kysely, PostgresDialect } from 'kysely';
|
|
399
470
|
import { Pool } from 'pg';
|
|
400
471
|
import type { DB } from './types/db';
|
|
@@ -417,7 +488,7 @@ function generateDbTypes() {
|
|
|
417
488
|
|
|
418
489
|
// Ping table types
|
|
419
490
|
export interface PingTable {
|
|
420
|
-
id: Generated<
|
|
491
|
+
id: Generated<number>;
|
|
421
492
|
message: string;
|
|
422
493
|
created_by: string;
|
|
423
494
|
created_at: Generated<Date>;
|
|
@@ -442,17 +513,61 @@ export const migrations = createMigrationProvider({
|
|
|
442
513
|
});
|
|
443
514
|
`;
|
|
444
515
|
}
|
|
445
|
-
function generatePingMigration() {
|
|
446
|
-
|
|
516
|
+
function generatePingMigration(dialect = "postgres") {
|
|
517
|
+
if (dialect === "sqlite") {
|
|
518
|
+
return `import { sql } from 'kysely';
|
|
447
519
|
import { defineMigration } from '@fossyl/kysely';
|
|
448
520
|
|
|
449
521
|
export const migration = defineMigration({
|
|
450
522
|
async up(db) {
|
|
451
523
|
await db.schema
|
|
452
524
|
.createTable('ping')
|
|
453
|
-
.addColumn('id', '
|
|
454
|
-
|
|
525
|
+
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
|
|
526
|
+
.addColumn('message', 'text', (col) => col.notNull())
|
|
527
|
+
.addColumn('created_by', 'text', (col) => col.notNull())
|
|
528
|
+
.addColumn('created_at', 'text', (col) =>
|
|
529
|
+
col.notNull().defaultTo(sql\`(datetime('now'))\`)
|
|
455
530
|
)
|
|
531
|
+
.execute();
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
async down(db) {
|
|
535
|
+
await db.schema.dropTable('ping').execute();
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
if (dialect === "mysql") {
|
|
541
|
+
return `import { sql } from 'kysely';
|
|
542
|
+
import { defineMigration } from '@fossyl/kysely';
|
|
543
|
+
|
|
544
|
+
export const migration = defineMigration({
|
|
545
|
+
async up(db) {
|
|
546
|
+
await db.schema
|
|
547
|
+
.createTable('ping')
|
|
548
|
+
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
|
|
549
|
+
.addColumn('message', 'varchar(255)', (col) => col.notNull())
|
|
550
|
+
.addColumn('created_by', 'varchar(255)', (col) => col.notNull())
|
|
551
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
552
|
+
col.notNull().defaultTo(sql\`CURRENT_TIMESTAMP\`)
|
|
553
|
+
)
|
|
554
|
+
.execute();
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
async down(db) {
|
|
558
|
+
await db.schema.dropTable('ping').execute();
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
return `import { sql } from 'kysely';
|
|
564
|
+
import { defineMigration } from '@fossyl/kysely';
|
|
565
|
+
|
|
566
|
+
export const migration = defineMigration({
|
|
567
|
+
async up(db) {
|
|
568
|
+
await db.schema
|
|
569
|
+
.createTable('ping')
|
|
570
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
456
571
|
.addColumn('message', 'varchar(255)', (col) => col.notNull())
|
|
457
572
|
.addColumn('created_by', 'varchar(255)', (col) => col.notNull())
|
|
458
573
|
.addColumn('created_at', 'timestamp', (col) =>
|
|
@@ -708,7 +823,7 @@ function generatePingService(_options) {
|
|
|
708
823
|
return `import * as pingRepo from '../repo/ping.repo';
|
|
709
824
|
|
|
710
825
|
export interface PingData {
|
|
711
|
-
id:
|
|
826
|
+
id: number;
|
|
712
827
|
message: string;
|
|
713
828
|
created_by: string;
|
|
714
829
|
created_at: Date;
|
|
@@ -784,7 +899,7 @@ export async function findById(id: string): Promise<Ping | undefined> {
|
|
|
784
899
|
const db = getTransaction<DB>();
|
|
785
900
|
return db
|
|
786
901
|
.selectFrom('ping')
|
|
787
|
-
.where('id', '=', id)
|
|
902
|
+
.where('id', '=', Number(id))
|
|
788
903
|
.selectAll()
|
|
789
904
|
.executeTakeFirst();
|
|
790
905
|
}
|
|
@@ -803,14 +918,14 @@ export async function update(id: string, data: PingUpdate): Promise<Ping> {
|
|
|
803
918
|
return db
|
|
804
919
|
.updateTable('ping')
|
|
805
920
|
.set(data)
|
|
806
|
-
.where('id', '=', id)
|
|
921
|
+
.where('id', '=', Number(id))
|
|
807
922
|
.returningAll()
|
|
808
923
|
.executeTakeFirstOrThrow();
|
|
809
924
|
}
|
|
810
925
|
|
|
811
926
|
export async function remove(id: string): Promise<void> {
|
|
812
927
|
const db = getTransaction<DB>();
|
|
813
|
-
await db.deleteFrom('ping').where('id', '=', id).execute();
|
|
928
|
+
await db.deleteFrom('ping').where('id', '=', Number(id)).execute();
|
|
814
929
|
}
|
|
815
930
|
`;
|
|
816
931
|
}
|
|
@@ -823,14 +938,15 @@ function generateByoPingRepo() {
|
|
|
823
938
|
*/
|
|
824
939
|
|
|
825
940
|
export interface Ping {
|
|
826
|
-
id:
|
|
941
|
+
id: number;
|
|
827
942
|
message: string;
|
|
828
943
|
created_by: string;
|
|
829
944
|
created_at: Date;
|
|
830
945
|
}
|
|
831
946
|
|
|
832
947
|
// In-memory store for demo purposes - replace with actual database
|
|
833
|
-
const pings: Map<
|
|
948
|
+
const pings: Map<number, Ping> = new Map();
|
|
949
|
+
let nextId = 1;
|
|
834
950
|
|
|
835
951
|
export async function findAll(limit: number, offset: number): Promise<Ping[]> {
|
|
836
952
|
// TODO: Replace with actual database query
|
|
@@ -840,13 +956,13 @@ export async function findAll(limit: number, offset: number): Promise<Ping[]> {
|
|
|
840
956
|
|
|
841
957
|
export async function findById(id: string): Promise<Ping | undefined> {
|
|
842
958
|
// TODO: Replace with actual database query
|
|
843
|
-
return pings.get(id);
|
|
959
|
+
return pings.get(Number(id));
|
|
844
960
|
}
|
|
845
961
|
|
|
846
962
|
export async function create(data: { message: string; created_by: string }): Promise<Ping> {
|
|
847
963
|
// TODO: Replace with actual database insert
|
|
848
964
|
const ping: Ping = {
|
|
849
|
-
id:
|
|
965
|
+
id: nextId++,
|
|
850
966
|
message: data.message,
|
|
851
967
|
created_by: data.created_by,
|
|
852
968
|
created_at: new Date(),
|
|
@@ -857,22 +973,115 @@ export async function create(data: { message: string; created_by: string }): Pro
|
|
|
857
973
|
|
|
858
974
|
export async function update(id: string, data: { message?: string }): Promise<Ping> {
|
|
859
975
|
// TODO: Replace with actual database update
|
|
860
|
-
const
|
|
976
|
+
const numId = Number(id);
|
|
977
|
+
const existing = pings.get(numId);
|
|
861
978
|
if (!existing) {
|
|
862
979
|
throw new Error('Not found');
|
|
863
980
|
}
|
|
864
981
|
const updated = { ...existing, ...data };
|
|
865
|
-
pings.set(
|
|
982
|
+
pings.set(numId, updated);
|
|
866
983
|
return updated;
|
|
867
984
|
}
|
|
868
985
|
|
|
869
986
|
export async function remove(id: string): Promise<void> {
|
|
870
987
|
// TODO: Replace with actual database delete
|
|
871
|
-
pings.delete(id);
|
|
988
|
+
pings.delete(Number(id));
|
|
872
989
|
}
|
|
873
990
|
`;
|
|
874
991
|
}
|
|
875
992
|
|
|
993
|
+
// src/templates/docker.ts
|
|
994
|
+
function generateDockerfile() {
|
|
995
|
+
return `FROM node:20-alpine
|
|
996
|
+
WORKDIR /app
|
|
997
|
+
COPY package*.json pnpm-lock.yaml* ./
|
|
998
|
+
RUN corepack enable && pnpm install --frozen-lockfile
|
|
999
|
+
COPY . .
|
|
1000
|
+
RUN pnpm build
|
|
1001
|
+
EXPOSE 3000
|
|
1002
|
+
CMD ["node", "dist/index.js"]
|
|
1003
|
+
`;
|
|
1004
|
+
}
|
|
1005
|
+
function generateDockerignore() {
|
|
1006
|
+
return `node_modules
|
|
1007
|
+
dist
|
|
1008
|
+
*.log
|
|
1009
|
+
.env
|
|
1010
|
+
.git
|
|
1011
|
+
`;
|
|
1012
|
+
}
|
|
1013
|
+
function generateDockerCompose(dialect) {
|
|
1014
|
+
if (dialect === "sqlite") {
|
|
1015
|
+
return `services:
|
|
1016
|
+
app:
|
|
1017
|
+
build: .
|
|
1018
|
+
ports:
|
|
1019
|
+
- "\${PORT:-3000}:3000"
|
|
1020
|
+
volumes:
|
|
1021
|
+
- ./data:/app/data
|
|
1022
|
+
environment:
|
|
1023
|
+
- DATABASE_PATH=/app/data/app.db
|
|
1024
|
+
`;
|
|
1025
|
+
}
|
|
1026
|
+
if (dialect === "mysql") {
|
|
1027
|
+
return `services:
|
|
1028
|
+
app:
|
|
1029
|
+
build: .
|
|
1030
|
+
ports:
|
|
1031
|
+
- "\${PORT:-3000}:3000"
|
|
1032
|
+
depends_on:
|
|
1033
|
+
db:
|
|
1034
|
+
condition: service_healthy
|
|
1035
|
+
environment:
|
|
1036
|
+
- DATABASE_URL=mysql://fossyl:fossyl@db:3306/fossyl
|
|
1037
|
+
db:
|
|
1038
|
+
image: mysql:8
|
|
1039
|
+
environment:
|
|
1040
|
+
MYSQL_ROOT_PASSWORD: root
|
|
1041
|
+
MYSQL_USER: fossyl
|
|
1042
|
+
MYSQL_PASSWORD: fossyl
|
|
1043
|
+
MYSQL_DATABASE: fossyl
|
|
1044
|
+
volumes:
|
|
1045
|
+
- mysqldata:/var/lib/mysql
|
|
1046
|
+
healthcheck:
|
|
1047
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
1048
|
+
interval: 5s
|
|
1049
|
+
timeout: 5s
|
|
1050
|
+
retries: 5
|
|
1051
|
+
|
|
1052
|
+
volumes:
|
|
1053
|
+
mysqldata:
|
|
1054
|
+
`;
|
|
1055
|
+
}
|
|
1056
|
+
return `services:
|
|
1057
|
+
app:
|
|
1058
|
+
build: .
|
|
1059
|
+
ports:
|
|
1060
|
+
- "\${PORT:-3000}:3000"
|
|
1061
|
+
depends_on:
|
|
1062
|
+
db:
|
|
1063
|
+
condition: service_healthy
|
|
1064
|
+
environment:
|
|
1065
|
+
- DATABASE_URL=postgres://fossyl:fossyl@db:5432/fossyl
|
|
1066
|
+
db:
|
|
1067
|
+
image: postgres:16-alpine
|
|
1068
|
+
environment:
|
|
1069
|
+
POSTGRES_USER: fossyl
|
|
1070
|
+
POSTGRES_PASSWORD: fossyl
|
|
1071
|
+
POSTGRES_DB: fossyl
|
|
1072
|
+
volumes:
|
|
1073
|
+
- pgdata:/var/lib/postgresql/data
|
|
1074
|
+
healthcheck:
|
|
1075
|
+
test: ["CMD-SHELL", "pg_isready -U fossyl"]
|
|
1076
|
+
interval: 5s
|
|
1077
|
+
timeout: 5s
|
|
1078
|
+
retries: 5
|
|
1079
|
+
|
|
1080
|
+
volumes:
|
|
1081
|
+
pgdata:
|
|
1082
|
+
`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
876
1085
|
// src/scaffold.ts
|
|
877
1086
|
function generateFiles(options) {
|
|
878
1087
|
const files = [];
|
|
@@ -910,7 +1119,7 @@ function generateFiles(options) {
|
|
|
910
1119
|
if (options.database === "kysely") {
|
|
911
1120
|
files.push({
|
|
912
1121
|
path: "src/db.ts",
|
|
913
|
-
content: generateKyselySetup()
|
|
1122
|
+
content: generateKyselySetup(options.dialect)
|
|
914
1123
|
});
|
|
915
1124
|
files.push({
|
|
916
1125
|
path: "src/types/db.ts",
|
|
@@ -922,7 +1131,7 @@ function generateFiles(options) {
|
|
|
922
1131
|
});
|
|
923
1132
|
files.push({
|
|
924
1133
|
path: "src/migrations/001_create_ping.ts",
|
|
925
|
-
content: generatePingMigration()
|
|
1134
|
+
content: generatePingMigration(options.dialect)
|
|
926
1135
|
});
|
|
927
1136
|
} else {
|
|
928
1137
|
files.push({
|
|
@@ -957,6 +1166,20 @@ function generateFiles(options) {
|
|
|
957
1166
|
path: "src/features/ping/repo/ping.repo.ts",
|
|
958
1167
|
content: generatePingRepo(options)
|
|
959
1168
|
});
|
|
1169
|
+
if (options.docker) {
|
|
1170
|
+
files.push({
|
|
1171
|
+
path: "Dockerfile",
|
|
1172
|
+
content: generateDockerfile()
|
|
1173
|
+
});
|
|
1174
|
+
files.push({
|
|
1175
|
+
path: ".dockerignore",
|
|
1176
|
+
content: generateDockerignore()
|
|
1177
|
+
});
|
|
1178
|
+
files.push({
|
|
1179
|
+
path: "docker-compose.yml",
|
|
1180
|
+
content: generateDockerCompose(options.dialect)
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
960
1183
|
return files;
|
|
961
1184
|
}
|
|
962
1185
|
function writeFiles(projectPath, files) {
|
package/dist/index.mjs
CHANGED
|
@@ -52,7 +52,30 @@ async function promptForOptions(projectName) {
|
|
|
52
52
|
p.cancel("Operation cancelled.");
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
let dialect;
|
|
56
|
+
if (database === "kysely") {
|
|
57
|
+
dialect = await p.select({
|
|
58
|
+
message: "Database dialect:",
|
|
59
|
+
options: [
|
|
60
|
+
{ value: "sqlite", label: "SQLite", hint: "recommended - great for per-customer databases" },
|
|
61
|
+
{ value: "postgres", label: "PostgreSQL" },
|
|
62
|
+
{ value: "mysql", label: "MySQL" }
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
if (p.isCancel(dialect)) {
|
|
66
|
+
p.cancel("Operation cancelled.");
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const docker = await p.confirm({
|
|
71
|
+
message: "Include Docker setup?",
|
|
72
|
+
initialValue: true
|
|
73
|
+
});
|
|
74
|
+
if (p.isCancel(docker)) {
|
|
75
|
+
p.cancel("Operation cancelled.");
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return { name, server, validator, database, dialect, docker };
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
// src/scaffold.ts
|
|
@@ -81,8 +104,15 @@ function generatePackageJson(options) {
|
|
|
81
104
|
if (options.database === "kysely") {
|
|
82
105
|
dependencies["@fossyl/kysely"] = "^0.9.0";
|
|
83
106
|
dependencies["kysely"] = "^0.27.0";
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
if (options.dialect === "sqlite") {
|
|
108
|
+
dependencies["better-sqlite3"] = "^11.0.0";
|
|
109
|
+
devDependencies["@types/better-sqlite3"] = "^7.6.0";
|
|
110
|
+
} else if (options.dialect === "mysql") {
|
|
111
|
+
dependencies["mysql2"] = "^3.11.0";
|
|
112
|
+
} else {
|
|
113
|
+
dependencies["pg"] = "^8.13.0";
|
|
114
|
+
devDependencies["@types/pg"] = "^8.11.0";
|
|
115
|
+
}
|
|
86
116
|
}
|
|
87
117
|
const pkg = {
|
|
88
118
|
name: options.name === "." ? "my-fossyl-api" : options.name,
|
|
@@ -123,8 +153,17 @@ PORT=3000
|
|
|
123
153
|
if (options.database === "kysely") {
|
|
124
154
|
content += `
|
|
125
155
|
# Database
|
|
126
|
-
DATABASE_URL=postgres://user:password@localhost:5432/mydb
|
|
127
156
|
`;
|
|
157
|
+
if (options.dialect === "sqlite") {
|
|
158
|
+
content += `DATABASE_PATH=./data/app.db
|
|
159
|
+
`;
|
|
160
|
+
} else if (options.dialect === "mysql") {
|
|
161
|
+
content += `DATABASE_URL=mysql://user:password@localhost:3306/mydb
|
|
162
|
+
`;
|
|
163
|
+
} else {
|
|
164
|
+
content += `DATABASE_URL=postgres://user:password@localhost:5432/mydb
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
128
167
|
}
|
|
129
168
|
return content;
|
|
130
169
|
}
|
|
@@ -370,7 +409,39 @@ export function startServer(routes: Route[], port: number): void {
|
|
|
370
409
|
}
|
|
371
410
|
|
|
372
411
|
// src/templates/database/kysely.ts
|
|
373
|
-
function generateKyselySetup() {
|
|
412
|
+
function generateKyselySetup(dialect = "postgres") {
|
|
413
|
+
if (dialect === "sqlite") {
|
|
414
|
+
return `import Database from 'better-sqlite3';
|
|
415
|
+
import { Kysely, SqliteDialect } from 'kysely';
|
|
416
|
+
import type { DB } from './types/db';
|
|
417
|
+
|
|
418
|
+
const databasePath = process.env.DATABASE_PATH || './data/app.db';
|
|
419
|
+
|
|
420
|
+
export const db = new Kysely<DB>({
|
|
421
|
+
dialect: new SqliteDialect({
|
|
422
|
+
database: new Database(databasePath),
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
if (dialect === "mysql") {
|
|
428
|
+
return `import { createPool } from 'mysql2';
|
|
429
|
+
import { Kysely, MysqlDialect } from 'kysely';
|
|
430
|
+
import type { DB } from './types/db';
|
|
431
|
+
|
|
432
|
+
const connectionString = process.env.DATABASE_URL;
|
|
433
|
+
|
|
434
|
+
if (!connectionString) {
|
|
435
|
+
throw new Error('DATABASE_URL environment variable is required');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export const db = new Kysely<DB>({
|
|
439
|
+
dialect: new MysqlDialect({
|
|
440
|
+
pool: createPool(connectionString),
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
`;
|
|
444
|
+
}
|
|
374
445
|
return `import { Kysely, PostgresDialect } from 'kysely';
|
|
375
446
|
import { Pool } from 'pg';
|
|
376
447
|
import type { DB } from './types/db';
|
|
@@ -393,7 +464,7 @@ function generateDbTypes() {
|
|
|
393
464
|
|
|
394
465
|
// Ping table types
|
|
395
466
|
export interface PingTable {
|
|
396
|
-
id: Generated<
|
|
467
|
+
id: Generated<number>;
|
|
397
468
|
message: string;
|
|
398
469
|
created_by: string;
|
|
399
470
|
created_at: Generated<Date>;
|
|
@@ -418,17 +489,61 @@ export const migrations = createMigrationProvider({
|
|
|
418
489
|
});
|
|
419
490
|
`;
|
|
420
491
|
}
|
|
421
|
-
function generatePingMigration() {
|
|
422
|
-
|
|
492
|
+
function generatePingMigration(dialect = "postgres") {
|
|
493
|
+
if (dialect === "sqlite") {
|
|
494
|
+
return `import { sql } from 'kysely';
|
|
423
495
|
import { defineMigration } from '@fossyl/kysely';
|
|
424
496
|
|
|
425
497
|
export const migration = defineMigration({
|
|
426
498
|
async up(db) {
|
|
427
499
|
await db.schema
|
|
428
500
|
.createTable('ping')
|
|
429
|
-
.addColumn('id', '
|
|
430
|
-
|
|
501
|
+
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
|
|
502
|
+
.addColumn('message', 'text', (col) => col.notNull())
|
|
503
|
+
.addColumn('created_by', 'text', (col) => col.notNull())
|
|
504
|
+
.addColumn('created_at', 'text', (col) =>
|
|
505
|
+
col.notNull().defaultTo(sql\`(datetime('now'))\`)
|
|
431
506
|
)
|
|
507
|
+
.execute();
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
async down(db) {
|
|
511
|
+
await db.schema.dropTable('ping').execute();
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
if (dialect === "mysql") {
|
|
517
|
+
return `import { sql } from 'kysely';
|
|
518
|
+
import { defineMigration } from '@fossyl/kysely';
|
|
519
|
+
|
|
520
|
+
export const migration = defineMigration({
|
|
521
|
+
async up(db) {
|
|
522
|
+
await db.schema
|
|
523
|
+
.createTable('ping')
|
|
524
|
+
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
|
|
525
|
+
.addColumn('message', 'varchar(255)', (col) => col.notNull())
|
|
526
|
+
.addColumn('created_by', 'varchar(255)', (col) => col.notNull())
|
|
527
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
528
|
+
col.notNull().defaultTo(sql\`CURRENT_TIMESTAMP\`)
|
|
529
|
+
)
|
|
530
|
+
.execute();
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
async down(db) {
|
|
534
|
+
await db.schema.dropTable('ping').execute();
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
return `import { sql } from 'kysely';
|
|
540
|
+
import { defineMigration } from '@fossyl/kysely';
|
|
541
|
+
|
|
542
|
+
export const migration = defineMigration({
|
|
543
|
+
async up(db) {
|
|
544
|
+
await db.schema
|
|
545
|
+
.createTable('ping')
|
|
546
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
432
547
|
.addColumn('message', 'varchar(255)', (col) => col.notNull())
|
|
433
548
|
.addColumn('created_by', 'varchar(255)', (col) => col.notNull())
|
|
434
549
|
.addColumn('created_at', 'timestamp', (col) =>
|
|
@@ -684,7 +799,7 @@ function generatePingService(_options) {
|
|
|
684
799
|
return `import * as pingRepo from '../repo/ping.repo';
|
|
685
800
|
|
|
686
801
|
export interface PingData {
|
|
687
|
-
id:
|
|
802
|
+
id: number;
|
|
688
803
|
message: string;
|
|
689
804
|
created_by: string;
|
|
690
805
|
created_at: Date;
|
|
@@ -760,7 +875,7 @@ export async function findById(id: string): Promise<Ping | undefined> {
|
|
|
760
875
|
const db = getTransaction<DB>();
|
|
761
876
|
return db
|
|
762
877
|
.selectFrom('ping')
|
|
763
|
-
.where('id', '=', id)
|
|
878
|
+
.where('id', '=', Number(id))
|
|
764
879
|
.selectAll()
|
|
765
880
|
.executeTakeFirst();
|
|
766
881
|
}
|
|
@@ -779,14 +894,14 @@ export async function update(id: string, data: PingUpdate): Promise<Ping> {
|
|
|
779
894
|
return db
|
|
780
895
|
.updateTable('ping')
|
|
781
896
|
.set(data)
|
|
782
|
-
.where('id', '=', id)
|
|
897
|
+
.where('id', '=', Number(id))
|
|
783
898
|
.returningAll()
|
|
784
899
|
.executeTakeFirstOrThrow();
|
|
785
900
|
}
|
|
786
901
|
|
|
787
902
|
export async function remove(id: string): Promise<void> {
|
|
788
903
|
const db = getTransaction<DB>();
|
|
789
|
-
await db.deleteFrom('ping').where('id', '=', id).execute();
|
|
904
|
+
await db.deleteFrom('ping').where('id', '=', Number(id)).execute();
|
|
790
905
|
}
|
|
791
906
|
`;
|
|
792
907
|
}
|
|
@@ -799,14 +914,15 @@ function generateByoPingRepo() {
|
|
|
799
914
|
*/
|
|
800
915
|
|
|
801
916
|
export interface Ping {
|
|
802
|
-
id:
|
|
917
|
+
id: number;
|
|
803
918
|
message: string;
|
|
804
919
|
created_by: string;
|
|
805
920
|
created_at: Date;
|
|
806
921
|
}
|
|
807
922
|
|
|
808
923
|
// In-memory store for demo purposes - replace with actual database
|
|
809
|
-
const pings: Map<
|
|
924
|
+
const pings: Map<number, Ping> = new Map();
|
|
925
|
+
let nextId = 1;
|
|
810
926
|
|
|
811
927
|
export async function findAll(limit: number, offset: number): Promise<Ping[]> {
|
|
812
928
|
// TODO: Replace with actual database query
|
|
@@ -816,13 +932,13 @@ export async function findAll(limit: number, offset: number): Promise<Ping[]> {
|
|
|
816
932
|
|
|
817
933
|
export async function findById(id: string): Promise<Ping | undefined> {
|
|
818
934
|
// TODO: Replace with actual database query
|
|
819
|
-
return pings.get(id);
|
|
935
|
+
return pings.get(Number(id));
|
|
820
936
|
}
|
|
821
937
|
|
|
822
938
|
export async function create(data: { message: string; created_by: string }): Promise<Ping> {
|
|
823
939
|
// TODO: Replace with actual database insert
|
|
824
940
|
const ping: Ping = {
|
|
825
|
-
id:
|
|
941
|
+
id: nextId++,
|
|
826
942
|
message: data.message,
|
|
827
943
|
created_by: data.created_by,
|
|
828
944
|
created_at: new Date(),
|
|
@@ -833,22 +949,115 @@ export async function create(data: { message: string; created_by: string }): Pro
|
|
|
833
949
|
|
|
834
950
|
export async function update(id: string, data: { message?: string }): Promise<Ping> {
|
|
835
951
|
// TODO: Replace with actual database update
|
|
836
|
-
const
|
|
952
|
+
const numId = Number(id);
|
|
953
|
+
const existing = pings.get(numId);
|
|
837
954
|
if (!existing) {
|
|
838
955
|
throw new Error('Not found');
|
|
839
956
|
}
|
|
840
957
|
const updated = { ...existing, ...data };
|
|
841
|
-
pings.set(
|
|
958
|
+
pings.set(numId, updated);
|
|
842
959
|
return updated;
|
|
843
960
|
}
|
|
844
961
|
|
|
845
962
|
export async function remove(id: string): Promise<void> {
|
|
846
963
|
// TODO: Replace with actual database delete
|
|
847
|
-
pings.delete(id);
|
|
964
|
+
pings.delete(Number(id));
|
|
848
965
|
}
|
|
849
966
|
`;
|
|
850
967
|
}
|
|
851
968
|
|
|
969
|
+
// src/templates/docker.ts
|
|
970
|
+
function generateDockerfile() {
|
|
971
|
+
return `FROM node:20-alpine
|
|
972
|
+
WORKDIR /app
|
|
973
|
+
COPY package*.json pnpm-lock.yaml* ./
|
|
974
|
+
RUN corepack enable && pnpm install --frozen-lockfile
|
|
975
|
+
COPY . .
|
|
976
|
+
RUN pnpm build
|
|
977
|
+
EXPOSE 3000
|
|
978
|
+
CMD ["node", "dist/index.js"]
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
function generateDockerignore() {
|
|
982
|
+
return `node_modules
|
|
983
|
+
dist
|
|
984
|
+
*.log
|
|
985
|
+
.env
|
|
986
|
+
.git
|
|
987
|
+
`;
|
|
988
|
+
}
|
|
989
|
+
function generateDockerCompose(dialect) {
|
|
990
|
+
if (dialect === "sqlite") {
|
|
991
|
+
return `services:
|
|
992
|
+
app:
|
|
993
|
+
build: .
|
|
994
|
+
ports:
|
|
995
|
+
- "\${PORT:-3000}:3000"
|
|
996
|
+
volumes:
|
|
997
|
+
- ./data:/app/data
|
|
998
|
+
environment:
|
|
999
|
+
- DATABASE_PATH=/app/data/app.db
|
|
1000
|
+
`;
|
|
1001
|
+
}
|
|
1002
|
+
if (dialect === "mysql") {
|
|
1003
|
+
return `services:
|
|
1004
|
+
app:
|
|
1005
|
+
build: .
|
|
1006
|
+
ports:
|
|
1007
|
+
- "\${PORT:-3000}:3000"
|
|
1008
|
+
depends_on:
|
|
1009
|
+
db:
|
|
1010
|
+
condition: service_healthy
|
|
1011
|
+
environment:
|
|
1012
|
+
- DATABASE_URL=mysql://fossyl:fossyl@db:3306/fossyl
|
|
1013
|
+
db:
|
|
1014
|
+
image: mysql:8
|
|
1015
|
+
environment:
|
|
1016
|
+
MYSQL_ROOT_PASSWORD: root
|
|
1017
|
+
MYSQL_USER: fossyl
|
|
1018
|
+
MYSQL_PASSWORD: fossyl
|
|
1019
|
+
MYSQL_DATABASE: fossyl
|
|
1020
|
+
volumes:
|
|
1021
|
+
- mysqldata:/var/lib/mysql
|
|
1022
|
+
healthcheck:
|
|
1023
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
1024
|
+
interval: 5s
|
|
1025
|
+
timeout: 5s
|
|
1026
|
+
retries: 5
|
|
1027
|
+
|
|
1028
|
+
volumes:
|
|
1029
|
+
mysqldata:
|
|
1030
|
+
`;
|
|
1031
|
+
}
|
|
1032
|
+
return `services:
|
|
1033
|
+
app:
|
|
1034
|
+
build: .
|
|
1035
|
+
ports:
|
|
1036
|
+
- "\${PORT:-3000}:3000"
|
|
1037
|
+
depends_on:
|
|
1038
|
+
db:
|
|
1039
|
+
condition: service_healthy
|
|
1040
|
+
environment:
|
|
1041
|
+
- DATABASE_URL=postgres://fossyl:fossyl@db:5432/fossyl
|
|
1042
|
+
db:
|
|
1043
|
+
image: postgres:16-alpine
|
|
1044
|
+
environment:
|
|
1045
|
+
POSTGRES_USER: fossyl
|
|
1046
|
+
POSTGRES_PASSWORD: fossyl
|
|
1047
|
+
POSTGRES_DB: fossyl
|
|
1048
|
+
volumes:
|
|
1049
|
+
- pgdata:/var/lib/postgresql/data
|
|
1050
|
+
healthcheck:
|
|
1051
|
+
test: ["CMD-SHELL", "pg_isready -U fossyl"]
|
|
1052
|
+
interval: 5s
|
|
1053
|
+
timeout: 5s
|
|
1054
|
+
retries: 5
|
|
1055
|
+
|
|
1056
|
+
volumes:
|
|
1057
|
+
pgdata:
|
|
1058
|
+
`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
852
1061
|
// src/scaffold.ts
|
|
853
1062
|
function generateFiles(options) {
|
|
854
1063
|
const files = [];
|
|
@@ -886,7 +1095,7 @@ function generateFiles(options) {
|
|
|
886
1095
|
if (options.database === "kysely") {
|
|
887
1096
|
files.push({
|
|
888
1097
|
path: "src/db.ts",
|
|
889
|
-
content: generateKyselySetup()
|
|
1098
|
+
content: generateKyselySetup(options.dialect)
|
|
890
1099
|
});
|
|
891
1100
|
files.push({
|
|
892
1101
|
path: "src/types/db.ts",
|
|
@@ -898,7 +1107,7 @@ function generateFiles(options) {
|
|
|
898
1107
|
});
|
|
899
1108
|
files.push({
|
|
900
1109
|
path: "src/migrations/001_create_ping.ts",
|
|
901
|
-
content: generatePingMigration()
|
|
1110
|
+
content: generatePingMigration(options.dialect)
|
|
902
1111
|
});
|
|
903
1112
|
} else {
|
|
904
1113
|
files.push({
|
|
@@ -933,6 +1142,20 @@ function generateFiles(options) {
|
|
|
933
1142
|
path: "src/features/ping/repo/ping.repo.ts",
|
|
934
1143
|
content: generatePingRepo(options)
|
|
935
1144
|
});
|
|
1145
|
+
if (options.docker) {
|
|
1146
|
+
files.push({
|
|
1147
|
+
path: "Dockerfile",
|
|
1148
|
+
content: generateDockerfile()
|
|
1149
|
+
});
|
|
1150
|
+
files.push({
|
|
1151
|
+
path: ".dockerignore",
|
|
1152
|
+
content: generateDockerignore()
|
|
1153
|
+
});
|
|
1154
|
+
files.push({
|
|
1155
|
+
path: "docker-compose.yml",
|
|
1156
|
+
content: generateDockerCompose(options.dialect)
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
936
1159
|
return files;
|
|
937
1160
|
}
|
|
938
1161
|
function writeFiles(projectPath, files) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fossyl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "CLI for scaffolding fossyl projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fossyl": "./bin/fossyl.js"
|
|
@@ -25,7 +25,15 @@
|
|
|
25
25
|
"cli",
|
|
26
26
|
"scaffolding",
|
|
27
27
|
"rest-api",
|
|
28
|
-
"typescript"
|
|
28
|
+
"typescript",
|
|
29
|
+
"generator",
|
|
30
|
+
"project-generator",
|
|
31
|
+
"create-app",
|
|
32
|
+
"starter",
|
|
33
|
+
"boilerplate",
|
|
34
|
+
"api-generator",
|
|
35
|
+
"nodejs",
|
|
36
|
+
"backend"
|
|
29
37
|
],
|
|
30
38
|
"author": "YoyoSaur",
|
|
31
39
|
"license": "GPL-3.0",
|