commit a188ee1426d13b4cdcbbd89965a138623414e9b2 Author: ๅคงๆตท Date: Sat Jun 6 10:40:48 2026 +0800 ๐ŸŽ‰ init: ๅฐ้พ™็š„ๅทฅไฝœ็ฉบ้—ด diff --git a/.benchmark/appointment/arch.json b/.benchmark/appointment/arch.json new file mode 100644 index 0000000..38cbd6c --- /dev/null +++ b/.benchmark/appointment/arch.json @@ -0,0 +1 @@ +{"projectName":"MyProject","domain":"generic","contract":{"projectName":"MyProject","domain":"generic","entities":[{"name":"User","table":"users","description":"็ณป็ปŸ็”จๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"ๅˆๅŒ","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"ๅฎกๆ‰นๆต็จ‹","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"ๅฎขๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"ๅˆฐๆœŸๆ้†’","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Services","table":"services","description":"ๆœๅŠก้กน็›ฎ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"duration_min","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"price","type":"DECIMAL(10,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Appointments","table":"appointments","description":"ๆ—ถ้—ดๆฎต้ข„็บฆ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"client_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"clients","fkColumn":"id"},{"name":"service_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"services","fkColumn":"id"},{"name":"start_time","type":"TIMESTAMPTZ","required":true,"isPrimary":false,"isAuto":false},{"name":"end_time","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"notes","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"clients","via":"client_id"},{"type":"belongsTo","entity":"services","via":"service_id"}]},{"name":"Clients","table":"clients","description":"ๅฎขๆˆท้€š็Ÿฅ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"data","type":"JSONB","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Stats","table":"stats","description":"้ข„็บฆ็ปŸ่ฎก่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"client_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"clients","fkColumn":"id"},{"name":"service_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"services","fkColumn":"id"},{"name":"start_time","type":"TIMESTAMPTZ","required":true,"isPrimary":false,"isAuto":false},{"name":"end_time","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"notes","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"clients","via":"client_id"},{"type":"belongsTo","entity":"services","via":"service_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","duration_min":"durationMin","client_id":"clientId","service_id":"serviceId","start_time":"startTime","end_time":"endTime","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","durationMin":"duration_min","clientId":"client_id","serviceId":"service_id","startTime":"start_time","endTime":"end_time","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Services":{"id":{"required":true},"name":{"required":true}},"Appointments":{"id":{"required":true},"start_time":{"required":true}},"Clients":{"id":{"required":true},"title":{"required":true}},"Stats":{"id":{"required":true},"start_time":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO๏ผˆๆ–‡ไปถๅญ˜ๅ‚จ๏ผ‰","deployment":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","considerations":[]},"architectureDiagram":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ MyProject โ€” System Architecture โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Client Layer โ”‚ โ”‚ Admin / Web โ”‚ โ”‚\nโ”‚ โ”‚ React Native 0.76 + Expo / Flutter 3.x โ”‚ โ”‚ React + Vite (Web) โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ API Gateway โ”‚ โ”‚\nโ”‚ โ”‚ Nginx / Kong โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Backend Services โ”‚ โ”‚\nโ”‚ โ”‚ NestJS + Prisma โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ PostgreSQL 15โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ MinIO โ”‚ โ”‚\nโ”‚ โ”‚ Primary โ”‚ โ”‚ Session โ”‚ โ”‚ Files โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ Modules: โ”‚\nโ”‚ 1 ๆœๅŠก้กน็›ฎ โ”‚\nโ”‚ 2 Schedule โ”‚\nโ”‚ 3 Notification โ”‚\nโ”‚ 4 Analytics โ”‚\nโ”‚ โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","dataFlows":[{"page":"ๆœๅŠก้กน็›ฎ","route":"/services","description":"ๆœๅŠก้กน็›ฎ","dataFlow":["GET /api/services โ†’ ่Žทๅ–ๆœๅŠก้กน็›ฎๅˆ—่กจ","POST /api/services โ†’ ๅˆ›ๅปบๆœๅŠก้กน็›ฎ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๆ—ถ้—ดๆฎต้ข„็บฆ","route":"/appointments","description":"ๆ—ถ้—ดๆฎต้ข„็บฆ","dataFlow":["GET /api/appointments โ†’ ่Žทๅ–ๆ—ถ้—ดๆฎต้ข„็บฆๅˆ—่กจ","POST /api/appointments โ†’ ๅˆ›ๅปบๆ—ถ้—ดๆฎต้ข„็บฆ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅฎขๆˆท้€š็Ÿฅ","route":"/clients","description":"ๅฎขๆˆท้€š็Ÿฅ","dataFlow":["GET /api/clients โ†’ ่Žทๅ–ๅฎขๆˆท้€š็Ÿฅๅˆ—่กจ","POST /api/clients โ†’ ๅˆ›ๅปบๅฎขๆˆท้€š็Ÿฅ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"้ข„็บฆ็ปŸ่ฎก","route":"/stats","description":"้ข„็บฆ็ปŸ่ฎก","dataFlow":["GET /api/stats โ†’ ่Žทๅ–้ข„็บฆ็ปŸ่ฎกๅˆ—่กจ","POST /api/stats โ†’ ๅˆ›ๅปบ้ข„็บฆ็ปŸ่ฎก"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"}],"modules":[{"name":"ๆœๅŠก้กน็›ฎ","label":"ๆœๅŠก้กน็›ฎ","features":["ๆœๅŠก้กน็›ฎ"],"responsibilities":["ๆไพ› ๆœๅŠก้กน็›ฎ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"schedule","label":"Schedule","features":["ๆ—ถ้—ดๆฎต้ข„็บฆ"],"responsibilities":["ๆไพ› ๆ—ถ้—ดๆฎต้ข„็บฆ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"notification","label":"Notification","features":["ๅฎขๆˆท้€š็Ÿฅ"],"responsibilities":["ๆไพ› ๅฎขๆˆท้€š็Ÿฅ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"analytics","label":"Analytics","features":["้ข„็บฆ็ปŸ่ฎก"],"responsibilities":["ๆไพ› ้ข„็บฆ็ปŸ่ฎก ็›ธๅ…ณๅŠŸ่ƒฝ"]}],"databaseSchema":[{"table":"users","description":"็”จๆˆท่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"services","description":"ๆœๅŠก้กน็›ฎ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"duration_min","type":"INTEGER"},{"name":"price","type":"DECIMAL(10,2)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_services_user ON services(user_id)"]},{"table":"appointments","description":"ๆ—ถ้—ดๆฎต้ข„็บฆ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"client_id","type":"UUID","constraints":"FK โ†’ clients.id"},{"name":"service_id","type":"UUID","constraints":"FK โ†’ services.id"},{"name":"start_time","type":"TIMESTAMPTZ","constraints":"NOT NULL"},{"name":"end_time","type":"TIMESTAMPTZ"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'pending'"},{"name":"notes","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_appointments_user ON appointments(user_id)"]},{"table":"clients","description":"ๅฎขๆˆท้€š็Ÿฅ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'active'"},{"name":"data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_clients_user ON clients(user_id)"]},{"table":"stats","description":"้ข„็บฆ็ปŸ่ฎก่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"client_id","type":"UUID","constraints":"FK โ†’ clients.id"},{"name":"service_id","type":"UUID","constraints":"FK โ†’ services.id"},{"name":"start_time","type":"TIMESTAMPTZ","constraints":"NOT NULL"},{"name":"end_time","type":"TIMESTAMPTZ"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'pending'"},{"name":"notes","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_stats_user ON stats(user_id)"]}],"apiDesign":[{"resource":"appointments","basePath":"/api/appointments","endpoints":[{"method":"GET","path":"/api/appointments","description":"่Žทๅ–ๆ—ถ้—ดๆฎต้ข„็บฆๅˆ—่กจ"},{"method":"POST","path":"/api/appointments","description":"ๅˆ›ๅปบๆ—ถ้—ดๆฎต้ข„็บฆ"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}]},{"resource":"clients","basePath":"/api/clients","endpoints":[{"method":"GET","path":"/api/clients","description":"่Žทๅ–ๅฎขๆˆท้€š็Ÿฅๅˆ—่กจ"},{"method":"POST","path":"/api/clients","description":"ๅˆ›ๅปบๅฎขๆˆท้€š็Ÿฅ"}]},{"resource":"services","basePath":"/api/services","endpoints":[{"method":"GET","path":"/api/services","description":"่Žทๅ–ๆœๅŠก้กน็›ฎๅˆ—่กจ"},{"method":"POST","path":"/api/services","description":"ๅˆ›ๅปบๆœๅŠก้กน็›ฎ"}]},{"resource":"stats","basePath":"/api/stats","endpoints":[{"method":"GET","path":"/api/stats","description":"่Žทๅ–้ข„็บฆ็ปŸ่ฎกๅˆ—่กจ"},{"method":"POST","path":"/api/stats","description":"ๅˆ›ๅปบ้ข„็บฆ็ปŸ่ฎก"}]}],"directoryStructure":["myproject/","โ”œโ”€โ”€ apps/","โ”‚ โ”œโ”€โ”€ mobile/ # React Native / Flutter ็งปๅŠจ็ซฏ","โ”‚ โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # ้กต้ข็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆœๅŠก้กน็›ฎ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schedule/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ้€š็”จ็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # ่‡ชๅฎšไน‰ Hooks","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API ่ฐƒ็”จๅฑ‚","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ # ็Šถๆ€็ฎก็†","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ","โ”‚ โ”‚ โ”œโ”€โ”€ app.json","โ”‚ โ”‚ โ””โ”€โ”€ package.json","โ”‚ โ””โ”€โ”€ web/ # Web ็ฎก็†ๅŽๅฐ","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ pages/","โ”‚ โ”‚ โ”œโ”€โ”€ components/","โ”‚ โ”‚ โ””โ”€โ”€ layouts/","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ packages/","โ”‚ โ””โ”€โ”€ shared/ # ๅ…ฑไบซ็ฑปๅž‹/ๅธธ้‡","โ”œโ”€โ”€ server/ # NestJS ๅŽ็ซฏๆœๅŠก","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ modules/ # ไธšๅŠกๆจกๅ—","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆœๅŠก้กน็›ฎ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆœๅŠก้กน็›ฎ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆœๅŠก้กน็›ฎ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆœๅŠก้กน็›ฎ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schedule/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schedule.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schedule.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schedule.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”œโ”€โ”€ common/ # ้€š็”จ๏ผˆguards, filters, interceptors๏ผ‰","โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ # Prisma ORM","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ schema.prisma","โ”‚ โ”‚ โ”œโ”€โ”€ config/ # ็Žฏๅขƒ้…็ฝฎ","โ”‚ โ”‚ โ””โ”€โ”€ main.ts","โ”‚ โ”œโ”€โ”€ test/","โ”‚ โ”œโ”€โ”€ Dockerfile","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ docker-compose.yml","โ”œโ”€โ”€ .github/workflows/ # CI/CD","โ”‚ โ””โ”€โ”€ ci.yml","โ”œโ”€โ”€ .env.example","โ”œโ”€โ”€ README.md","โ””โ”€โ”€ turbo.json # Monorepo ้…็ฝฎ"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions โ†’ Build โ†’ Test โ†’ Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:37.851Z","sourceDomain":"generic","moduleCount":4,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/appointment/backend/.gitignore b/.benchmark/appointment/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/appointment/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/appointment/backend/README.md b/.benchmark/appointment/backend/README.md new file mode 100644 index 0000000..0c5cff6 --- /dev/null +++ b/.benchmark/appointment/backend/README.md @@ -0,0 +1,118 @@ +# MyProject โ€” Backend API + +> ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### services +- `GET /api/services` โ€” List all +- `GET /api/services/:id` โ€” Get by ID +- `POST /api/services` โ€” Create +- `PUT /api/services/:id` โ€” Update +- `DELETE /api/services/:id` โ€” Delete + +#### appointments +- `GET /api/appointments` โ€” List all +- `GET /api/appointments/:id` โ€” Get by ID +- `POST /api/appointments` โ€” Create +- `PUT /api/appointments/:id` โ€” Update +- `DELETE /api/appointments/:id` โ€” Delete + +#### clients +- `GET /api/clients` โ€” List all +- `GET /api/clients/:id` โ€” Get by ID +- `POST /api/clients` โ€” Create +- `PUT /api/clients/:id` โ€” Update +- `DELETE /api/clients/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List all +- `GET /api/stats/:id` โ€” Get by ID +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/appointment/backend/package.json b/.benchmark/appointment/backend/package.json new file mode 100644 index 0000000..e3106a0 --- /dev/null +++ b/.benchmark/appointment/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/backend/src/__tests__/appointments.test.ts b/.benchmark/appointment/backend/src/__tests__/appointments.test.ts new file mode 100644 index 0000000..5aa044f --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/appointments.test.ts @@ -0,0 +1,121 @@ +// Appointments CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "clientId": "sample-clientid", + "serviceId": "sample-serviceid", + "startTime": "sample-starttime", + "endTime": "sample-endtime", + "notes": "sample-notes" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/appointments", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/appointments", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/appointments", () => { + it("creates a appointment", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/appointments", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/appointments/:id", () => { + it("returns the created appointment", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/appointments/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/appointments/:id", () => { + it("updates the appointment", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/appointments/:id", () => { + it("deletes the appointment", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/appointments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/approvals.test.ts b/.benchmark/appointment/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/auth.test.ts b/.benchmark/appointment/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/clients.test.ts b/.benchmark/appointment/backend/src/__tests__/clients.test.ts new file mode 100644 index 0000000..35e6627 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/clients.test.ts @@ -0,0 +1,119 @@ +// Clients CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/clients", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/clients", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/clients", () => { + it("creates a client", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/clients", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/clients/:id", () => { + it("returns the created client", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/clients/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/clients/:id", () => { + it("updates the client", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/clients/:id", () => { + it("deletes the client", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/clients/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/contracts.test.ts b/.benchmark/appointment/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/customers.test.ts b/.benchmark/appointment/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/items.test.ts b/.benchmark/appointment/backend/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/reminders.test.ts b/.benchmark/appointment/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/services.test.ts b/.benchmark/appointment/backend/src/__tests__/services.test.ts new file mode 100644 index 0000000..cc8293c --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/services.test.ts @@ -0,0 +1,120 @@ +// Services CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "description": "sample-description", + "durationMin": 1, + "price": 1 + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/services", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/services", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/services", () => { + it("creates a service", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/services", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/services/:id", () => { + it("returns the created service", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/services/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/services/:id", () => { + it("updates the service", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/services/:id", () => { + it("deletes the service", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/services/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/stats.test.ts b/.benchmark/appointment/backend/src/__tests__/stats.test.ts new file mode 100644 index 0000000..de14005 --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/stats.test.ts @@ -0,0 +1,121 @@ +// Stats CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "clientId": "sample-clientid", + "serviceId": "sample-serviceid", + "startTime": "sample-starttime", + "endTime": "sample-endtime", + "notes": "sample-notes" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/stats", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/stats", () => { + it("creates a stat", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/stats/:id", () => { + it("returns the created stat", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/stats/:id", () => { + it("updates the stat", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/stats/:id", () => { + it("deletes the stat", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/__tests__/users.test.ts b/.benchmark/appointment/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/appointment/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/backend/src/db/client.ts b/.benchmark/appointment/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/appointment/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/appointment/backend/src/db/schema.ts b/.benchmark/appointment/backend/src/db/schema.ts new file mode 100644 index 0000000..ef6555c --- /dev/null +++ b/.benchmark/appointment/backend/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS services (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n description TEXT,\n duration_min INTEGER,\n price REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS appointments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS clients (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/appointment/backend/src/index.ts b/.benchmark/appointment/backend/src/index.ts new file mode 100644 index 0000000..7d4b2cb --- /dev/null +++ b/.benchmark/appointment/backend/src/index.ts @@ -0,0 +1,79 @@ +// MyProject โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { servicesRoutes } from "./routes/services.js"; +import { appointmentsRoutes } from "./routes/appointments.js"; +import { clientsRoutes } from "./routes/clients.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-2d3e1114"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(servicesRoutes, { prefix: "/api/services" }); + await app.register(appointmentsRoutes, { prefix: "/api/appointments" }); + await app.register(clientsRoutes, { prefix: "/api/clients" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/appointment/backend/src/middleware/auth.ts b/.benchmark/appointment/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/appointment/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/appointment/backend/src/routes/appointments.ts b/.benchmark/appointment/backend/src/routes/appointments.ts new file mode 100644 index 0000000..b4c9882 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/appointments.ts @@ -0,0 +1,51 @@ +// Auto-generated Appointments routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { AppointmentsService } from "../services/appointment.js"; +import type { CreateAppointmentsInput, UpdateAppointmentsInput } from "../types/index.js"; + +const service = new AppointmentsService(); + +export async function appointmentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/appointments โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/appointments/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Appointments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/appointments โ€” create + app.post<{ Body: CreateAppointmentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/appointments/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateAppointmentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Appointments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/appointments/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Appointments not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/approvals.ts b/.benchmark/appointment/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals โ€” create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/auth.ts b/.benchmark/appointment/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/appointment/backend/src/routes/clients.ts b/.benchmark/appointment/backend/src/routes/clients.ts new file mode 100644 index 0000000..0b10f84 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/clients.ts @@ -0,0 +1,51 @@ +// Auto-generated Clients routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ClientsService } from "../services/client.js"; +import type { CreateClientsInput, UpdateClientsInput } from "../types/index.js"; + +const service = new ClientsService(); + +export async function clientsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/clients โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/clients/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Clients not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/clients โ€” create + app.post<{ Body: CreateClientsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/clients/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateClientsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Clients not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/clients/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Clients not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/contracts.ts b/.benchmark/appointment/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts โ€” create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/customers.ts b/.benchmark/appointment/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers โ€” create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/items.ts b/.benchmark/appointment/backend/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items โ€” create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/reminders.ts b/.benchmark/appointment/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders โ€” create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/services.ts b/.benchmark/appointment/backend/src/routes/services.ts new file mode 100644 index 0000000..f280933 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/services.ts @@ -0,0 +1,51 @@ +// Auto-generated Services routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ServicesService } from "../services/service.js"; +import type { CreateServicesInput, UpdateServicesInput } from "../types/index.js"; + +const service = new ServicesService(); + +export async function servicesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/services โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/services/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Services not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/services โ€” create + app.post<{ Body: CreateServicesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/services/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateServicesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Services not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/services/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Services not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/stats.ts b/.benchmark/appointment/backend/src/routes/stats.ts new file mode 100644 index 0000000..92f64ea --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/stats.ts @@ -0,0 +1,51 @@ +// Auto-generated Stats routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StatsService } from "../services/stat.js"; +import type { CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +const service = new StatsService(); + +export async function statsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/stats โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/stats/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/stats โ€” create + app.post<{ Body: CreateStatsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/stats/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateStatsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/stats/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/routes/users.ts b/.benchmark/appointment/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/appointment/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/backend/src/services/appointment.ts b/.benchmark/appointment/backend/src/services/appointment.ts new file mode 100644 index 0000000..337572e --- /dev/null +++ b/.benchmark/appointment/backend/src/services/appointment.ts @@ -0,0 +1,56 @@ +// Auto-generated Appointments service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Appointments, CreateAppointmentsInput, UpdateAppointmentsInput } from "../types/index.js"; + +export class AppointmentsService { + /** List all appointments */ + list(): Appointments[] { + return queryAll("SELECT * FROM appointments ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Appointments | undefined { + return queryOne("SELECT * FROM appointments WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateAppointmentsInput): Appointments { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "client_id", "service_id", "start_time", "end_time", "notes", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.clientId ?? null, input.serviceId ?? null, input.startTime ?? null, input.endTime ?? null, input.notes ?? null, now, now]; + + execute(`INSERT INTO appointments (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateAppointmentsInput): Appointments | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.clientId !== undefined) { sets.push("client_id = ?"); values.push(input.clientId); } + if (input.serviceId !== undefined) { sets.push("service_id = ?"); values.push(input.serviceId); } + if (input.startTime !== undefined) { sets.push("start_time = ?"); values.push(input.startTime); } + if (input.endTime !== undefined) { sets.push("end_time = ?"); values.push(input.endTime); } + if (input.notes !== undefined) { sets.push("notes = ?"); values.push(input.notes); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE appointments SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM appointments WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/approval.ts b/.benchmark/appointment/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/client.ts b/.benchmark/appointment/backend/src/services/client.ts new file mode 100644 index 0000000..b0b1747 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/client.ts @@ -0,0 +1,54 @@ +// Auto-generated Clients service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Clients, CreateClientsInput, UpdateClientsInput } from "../types/index.js"; + +export class ClientsService { + /** List all clients */ + list(): Clients[] { + return queryAll("SELECT * FROM clients ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Clients | undefined { + return queryOne("SELECT * FROM clients WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateClientsInput): Clients { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO clients (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateClientsInput): Clients | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE clients SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM clients WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/contract.ts b/.benchmark/appointment/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/customer.ts b/.benchmark/appointment/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/item.ts b/.benchmark/appointment/backend/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/reminder.ts b/.benchmark/appointment/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/service.ts b/.benchmark/appointment/backend/src/services/service.ts new file mode 100644 index 0000000..0a4b4c0 --- /dev/null +++ b/.benchmark/appointment/backend/src/services/service.ts @@ -0,0 +1,55 @@ +// Auto-generated Services service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Services, CreateServicesInput, UpdateServicesInput } from "../types/index.js"; + +export class ServicesService { + /** List all services */ + list(): Services[] { + return queryAll("SELECT * FROM services ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Services | undefined { + return queryOne("SELECT * FROM services WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateServicesInput): Services { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "description", "duration_min", "price", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.description ?? null, input.durationMin ?? null, input.price ?? null, now, now]; + + execute(`INSERT INTO services (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateServicesInput): Services | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.durationMin !== undefined) { sets.push("duration_min = ?"); values.push(input.durationMin); } + if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE services SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM services WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/stat.ts b/.benchmark/appointment/backend/src/services/stat.ts new file mode 100644 index 0000000..ffdf1ec --- /dev/null +++ b/.benchmark/appointment/backend/src/services/stat.ts @@ -0,0 +1,56 @@ +// Auto-generated Stats service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Stats, CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +export class StatsService { + /** List all stats */ + list(): Stats[] { + return queryAll("SELECT * FROM stats ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Stats | undefined { + return queryOne("SELECT * FROM stats WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStatsInput): Stats { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "client_id", "service_id", "start_time", "end_time", "notes", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.clientId ?? null, input.serviceId ?? null, input.startTime ?? null, input.endTime ?? null, input.notes ?? null, now, now]; + + execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStatsInput): Stats | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.clientId !== undefined) { sets.push("client_id = ?"); values.push(input.clientId); } + if (input.serviceId !== undefined) { sets.push("service_id = ?"); values.push(input.serviceId); } + if (input.startTime !== undefined) { sets.push("start_time = ?"); values.push(input.startTime); } + if (input.endTime !== undefined) { sets.push("end_time = ?"); values.push(input.endTime); } + if (input.notes !== undefined) { sets.push("notes = ?"); values.push(input.notes); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM stats WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/services/user.ts b/.benchmark/appointment/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/appointment/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/backend/src/types/fastify.d.ts b/.benchmark/appointment/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/appointment/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/appointment/backend/src/types/index.ts b/.benchmark/appointment/backend/src/types/index.ts new file mode 100644 index 0000000..d6ab46d --- /dev/null +++ b/.benchmark/appointment/backend/src/types/index.ts @@ -0,0 +1,322 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateServicesInput { + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface CreateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface CreateClientsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateServicesInput { + userId?: string; + name?: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface UpdateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +export interface UpdateClientsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/appointment/backend/src/types/sql.js.d.ts b/.benchmark/appointment/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/appointment/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/appointment/backend/tsconfig.json b/.benchmark/appointment/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/appointment/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/electron/README.md b/.benchmark/appointment/electron/README.md new file mode 100644 index 0000000..cb32bc8 --- /dev/null +++ b/.benchmark/appointment/electron/README.md @@ -0,0 +1,82 @@ +# myproject โ€” Desktop + +> Electron desktop application wrapping the myproject fullstack web project. + +## Architecture + +``` +Web Fullstack Project + ร— + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** โ€” Window management, menu, tray, IPC, auto-update +- **Preload** โ€” Secure bridge between main and renderer +- **Renderer** โ€” Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +โ”œโ”€โ”€ main.ts โ€” Main process (BrowserWindow, Menu, lifecycle) +โ”œโ”€โ”€ preload.ts โ€” Secure IPC bridge (contextBridge) +โ”œโ”€โ”€ ipc.ts โ€” IPC handlers (dialogs, store, notifications) +โ”œโ”€โ”€ tray.ts โ€” System tray +โ”œโ”€โ”€ updater.ts โ€” Auto-update (electron-updater) +โ””โ”€โ”€ utils.ts โ€” Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/appointment/electron/assets/icon.png b/.benchmark/appointment/electron/assets/icon.png new file mode 100644 index 0000000..554e8a3 Binary files /dev/null and b/.benchmark/appointment/electron/assets/icon.png differ diff --git a/.benchmark/appointment/electron/electron-builder.yml b/.benchmark/appointment/electron/electron-builder.yml new file mode 100644 index 0000000..1ab271c --- /dev/null +++ b/.benchmark/appointment/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml โ€” Auto-generated by SF-06 + +appId: com.myproject.desktop +productName: myproject +copyright: "Copyright ยฉ 2026 myproject" + +# โ”€โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +directories: + output: release + buildResources: assets + +# โ”€โ”€โ”€ Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# โ”€โ”€โ”€ Extra Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# โ”€โ”€โ”€ macOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# โ”€โ”€โ”€ Windows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: myproject + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: myproject + +# โ”€โ”€โ”€ Linux โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: myproject + Comment: myproject Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# โ”€โ”€โ”€ Auto Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +publish: + provider: generic + url: https://releases.example.com/myproject/ + channel: latest diff --git a/.benchmark/appointment/electron/electron/ipc.ts b/.benchmark/appointment/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/appointment/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // โ”€โ”€ App Info โ”€โ”€ + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // โ”€โ”€ Window Controls โ”€โ”€ + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // โ”€โ”€ File Dialogs โ”€โ”€ + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // โ”€โ”€ Notifications โ”€โ”€ + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // โ”€โ”€ Shell โ”€โ”€ + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // โ”€โ”€ Persistent Store โ”€โ”€ + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/appointment/electron/electron/main.ts b/.benchmark/appointment/electron/electron/main.ts new file mode 100644 index 0000000..eaab114 --- /dev/null +++ b/.benchmark/appointment/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "myproject"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// โ”€โ”€โ”€ Create Window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // โ”€โ”€ Load content โ”€โ”€ + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // โ”€โ”€ Show when ready โ”€โ”€ + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // โ”€โ”€ External links โ†’ default browser โ”€โ”€ + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // โ”€โ”€ Prevent navigation away โ”€โ”€ + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// โ”€โ”€โ”€ Icon Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// โ”€โ”€โ”€ API Server (Production) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// โ”€โ”€โ”€ Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// โ”€โ”€โ”€ App Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed โ€” continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/appointment/electron/electron/preload.ts b/.benchmark/appointment/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/appointment/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// โ”€โ”€โ”€ Expose safe API to renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +contextBridge.exposeInMainWorld("electronAPI", { + // โ”€โ”€ App Info โ”€โ”€ + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // โ”€โ”€ Window Controls โ”€โ”€ + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // โ”€โ”€ File Dialogs โ”€โ”€ + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // โ”€โ”€ Notifications โ”€โ”€ + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // โ”€โ”€ Shell โ”€โ”€ + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // โ”€โ”€ Store โ”€โ”€ + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // โ”€โ”€ Updates โ”€โ”€ + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // โ”€โ”€ Update Events โ”€โ”€ + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // โ”€โ”€ Remove listener โ”€โ”€ + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// โ”€โ”€โ”€ Type declarations for renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/appointment/electron/electron/tray.ts b/.benchmark/appointment/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/appointment/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/appointment/electron/electron/updater.ts b/.benchmark/appointment/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/appointment/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/appointment/electron/electron/utils.ts b/.benchmark/appointment/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/appointment/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// โ”€โ”€โ”€ Dev mode detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function isDev(): boolean { + return !app.isPackaged; +} + +// โ”€โ”€โ”€ Web URL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// โ”€โ”€โ”€ API Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// โ”€โ”€โ”€ Get API port from env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/appointment/electron/entitlements.mac.plist b/.benchmark/appointment/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/appointment/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/appointment/electron/package.json b/.benchmark/appointment/electron/package.json new file mode 100644 index 0000000..44bc859 --- /dev/null +++ b/.benchmark/appointment/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "myproject-desktop", + "version": "1.0.0", + "private": true, + "description": "myproject โ€” Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "myproject", + "appId": "com.myproject.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/appointment/electron/tsconfig.json b/.benchmark/appointment/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/appointment/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/frontend/app/appointments/page.tsx b/.benchmark/appointment/frontend/app/appointments/page.tsx new file mode 100644 index 0000000..0bf4ee6 --- /dev/null +++ b/.benchmark/appointment/frontend/app/appointments/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useAppointments } from "@/hooks/useAppointments"; + +export default function PagePage() { + const { data, loading, error, refetch } = useAppointments(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆ—ถ้—ดๆฎต้ข„็บฆ

+

ๆ—ถ้—ดๆฎต้ข„็บฆ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/clients/page.tsx b/.benchmark/appointment/frontend/app/clients/page.tsx new file mode 100644 index 0000000..c4c750c --- /dev/null +++ b/.benchmark/appointment/frontend/app/clients/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useClients } from "@/hooks/useClients"; + +export default function PagePage() { + const { data, loading, error, refetch } = useClients(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅฎขๆˆท้€š็Ÿฅ

+

ๅฎขๆˆท้€š็Ÿฅ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/feature/page.tsx b/.benchmark/appointment/frontend/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/appointment/frontend/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅŠŸ่ƒฝ้กต

+

ๆ ธๅฟƒๅŠŸ่ƒฝ้กต้ข

+
+ +
+ + { +

ๅŠŸ่ƒฝ้กต ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /feature

+
} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/globals.css b/.benchmark/appointment/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/appointment/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/appointment/frontend/app/layout.tsx b/.benchmark/appointment/frontend/app/layout.tsx new file mode 100644 index 0000000..84b9b86 --- /dev/null +++ b/.benchmark/appointment/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/appointment/frontend/app/page.tsx b/.benchmark/appointment/frontend/app/page.tsx new file mode 100644 index 0000000..e7a78d1 --- /dev/null +++ b/.benchmark/appointment/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ๆœๅŠก้กน็›ฎ + + + ๐Ÿ“… + ๆ—ถ้—ดๆฎต้ข„็บฆ + + + ๐Ÿ“ + ๅฎขๆˆท้€š็Ÿฅ + + + ๐Ÿ“ธ + ้ข„็บฆ็ปŸ่ฎก + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ MyProject

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/appointment/frontend/app/profile/page.tsx b/.benchmark/appointment/frontend/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/appointment/frontend/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆˆ‘็š„

+

็”จๆˆทไธญๅฟƒ

+
+ +
+ + { +

ๆˆ‘็š„ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile

+
} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/services/page.tsx b/.benchmark/appointment/frontend/app/services/page.tsx new file mode 100644 index 0000000..4b3e8b6 --- /dev/null +++ b/.benchmark/appointment/frontend/app/services/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useServices } from "@/hooks/useServices"; + +export default function PagePage() { + const { data, loading, error, refetch } = useServices(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆœๅŠก้กน็›ฎ

+

ๆœๅŠก้กน็›ฎ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/stats/page.tsx b/.benchmark/appointment/frontend/app/stats/page.tsx new file mode 100644 index 0000000..3b8d151 --- /dev/null +++ b/.benchmark/appointment/frontend/app/stats/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useStats } from "@/hooks/useStats"; + +export default function PagePage() { + const { data, loading, error, refetch } = useStats(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

้ข„็บฆ็ปŸ่ฎก

+

้ข„็บฆ็ปŸ่ฎก

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/ๅฎขๆˆท้€š็Ÿฅ/page.tsx b/.benchmark/appointment/frontend/app/ๅฎขๆˆท้€š็Ÿฅ/page.tsx new file mode 100644 index 0000000..3213ba2 --- /dev/null +++ b/.benchmark/appointment/frontend/app/ๅฎขๆˆท้€š็Ÿฅ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๅฎขๆˆท้€š็Ÿฅ } from "@/hooks/useๅฎขๆˆท้€š็Ÿฅ"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๅฎขๆˆท้€š็Ÿฅ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅฎขๆˆท้€š็Ÿฅ

+

ๅฎขๆˆท้€š็Ÿฅ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/ๆ—ถ้—ดๆฎต้ข„็บฆ/page.tsx b/.benchmark/appointment/frontend/app/ๆ—ถ้—ดๆฎต้ข„็บฆ/page.tsx new file mode 100644 index 0000000..b4c9b36 --- /dev/null +++ b/.benchmark/appointment/frontend/app/ๆ—ถ้—ดๆฎต้ข„็บฆ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๆ—ถ้—ดๆฎต้ข„็บฆ } from "@/hooks/useๆ—ถ้—ดๆฎต้ข„็บฆ"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๆ—ถ้—ดๆฎต้ข„็บฆ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆ—ถ้—ดๆฎต้ข„็บฆ

+

ๆ—ถ้—ดๆฎต้ข„็บฆ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/ๆœๅŠก้กน็›ฎ/page.tsx b/.benchmark/appointment/frontend/app/ๆœๅŠก้กน็›ฎ/page.tsx new file mode 100644 index 0000000..5832ccc --- /dev/null +++ b/.benchmark/appointment/frontend/app/ๆœๅŠก้กน็›ฎ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๆœๅŠก้กน็›ฎ } from "@/hooks/useๆœๅŠก้กน็›ฎ"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๆœๅŠก้กน็›ฎ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆœๅŠก้กน็›ฎ

+

ๆœๅŠก้กน็›ฎ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/app/้ข„็บฆ็ปŸ่ฎก/page.tsx b/.benchmark/appointment/frontend/app/้ข„็บฆ็ปŸ่ฎก/page.tsx new file mode 100644 index 0000000..35fa74c --- /dev/null +++ b/.benchmark/appointment/frontend/app/้ข„็บฆ็ปŸ่ฎก/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use้ข„็บฆ็ปŸ่ฎก } from "@/hooks/use้ข„็บฆ็ปŸ่ฎก"; + +export default function PagePage() { + const { data, loading, error, refetch } = use้ข„็บฆ็ปŸ่ฎก(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

้ข„็บฆ็ปŸ่ฎก

+

้ข„็บฆ็ปŸ่ฎก

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/layout/BottomNav.tsx b/.benchmark/appointment/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/appointment/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/appointment/frontend/components/layout/Sidebar.tsx b/.benchmark/appointment/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/appointment/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Button.tsx b/.benchmark/appointment/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Card.tsx b/.benchmark/appointment/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/ui/EmptyState.tsx b/.benchmark/appointment/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Input.tsx b/.benchmark/appointment/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/appointment/frontend/components/ui/Modal.tsx b/.benchmark/appointment/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/appointment/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/appointment/frontend/hooks/useAppointments.ts b/.benchmark/appointment/frontend/hooks/useAppointments.ts new file mode 100644 index 0000000..930eac5 --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useAppointments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for appointments +type Appointment = Record; + +/** + * Fetch and manage appointments data. + */ +export function useAppointments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/appointments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useAuth.ts b/.benchmark/appointment/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useClients.ts b/.benchmark/appointment/frontend/hooks/useClients.ts new file mode 100644 index 0000000..86d98db --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useClients.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for clients +type Client = Record; + +/** + * Fetch and manage clients data. + */ +export function useClients() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/clients`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useDebounce.ts b/.benchmark/appointment/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/appointment/frontend/hooks/useForm.ts b/.benchmark/appointment/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/appointment/frontend/hooks/useHealth.ts b/.benchmark/appointment/frontend/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useItem.ts b/.benchmark/appointment/frontend/hooks/useItem.ts new file mode 100644 index 0000000..83b19eb --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for ้ข„็บฆ็ปŸ่ฎก +type Item = Record; + +/** + * Fetch and manage ้ข„็บฆ็ปŸ่ฎก data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/้ข„็บฆ็ปŸ่ฎก`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useServices.ts b/.benchmark/appointment/frontend/hooks/useServices.ts new file mode 100644 index 0000000..c0bf950 --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useServices.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for services +type Service = Record; + +/** + * Fetch and manage services data. + */ +export function useServices() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/services`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useStats.ts b/.benchmark/appointment/frontend/hooks/useStats.ts new file mode 100644 index 0000000..f9f34cf --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useStats.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for stats +type Stat = Record; + +/** + * Fetch and manage stats data. + */ +export function useStats() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/stats`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/hooks/useUsers.ts b/.benchmark/appointment/frontend/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/appointment/frontend/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/frontend/next-env.d.ts b/.benchmark/appointment/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/appointment/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/appointment/frontend/next.config.ts b/.benchmark/appointment/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/appointment/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/appointment/frontend/package.json b/.benchmark/appointment/frontend/package.json new file mode 100644 index 0000000..caa9eab --- /dev/null +++ b/.benchmark/appointment/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/frontend/postcss.config.js b/.benchmark/appointment/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/appointment/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/appointment/frontend/services/api.ts b/.benchmark/appointment/frontend/services/api.ts new file mode 100644 index 0000000..13abf7f --- /dev/null +++ b/.benchmark/appointment/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for MyProject + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/appointment/frontend/services/appointments.ts b/.benchmark/appointment/frontend/services/appointments.ts new file mode 100644 index 0000000..efbf64e --- /dev/null +++ b/.benchmark/appointment/frontend/services/appointments.ts @@ -0,0 +1,10 @@ +// Auto-generated appointments service +import { api } from "./api"; + +export function get() { + return api.get("/api/appointments"); +} + +export function post(data: Record) { + return api.post("/api/appointments", data); +} diff --git a/.benchmark/appointment/frontend/services/auth.ts b/.benchmark/appointment/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/appointment/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/appointment/frontend/services/clients.ts b/.benchmark/appointment/frontend/services/clients.ts new file mode 100644 index 0000000..43638ef --- /dev/null +++ b/.benchmark/appointment/frontend/services/clients.ts @@ -0,0 +1,10 @@ +// Auto-generated clients service +import { api } from "./api"; + +export function get() { + return api.get("/api/clients"); +} + +export function post(data: Record) { + return api.post("/api/clients", data); +} diff --git a/.benchmark/appointment/frontend/services/health.ts b/.benchmark/appointment/frontend/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/appointment/frontend/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/appointment/frontend/services/services.ts b/.benchmark/appointment/frontend/services/services.ts new file mode 100644 index 0000000..cdbe56a --- /dev/null +++ b/.benchmark/appointment/frontend/services/services.ts @@ -0,0 +1,10 @@ +// Auto-generated services service +import { api } from "./api"; + +export function get() { + return api.get("/api/services"); +} + +export function post(data: Record) { + return api.post("/api/services", data); +} diff --git a/.benchmark/appointment/frontend/services/stats.ts b/.benchmark/appointment/frontend/services/stats.ts new file mode 100644 index 0000000..e48d921 --- /dev/null +++ b/.benchmark/appointment/frontend/services/stats.ts @@ -0,0 +1,10 @@ +// Auto-generated stats service +import { api } from "./api"; + +export function get() { + return api.get("/api/stats"); +} + +export function post(data: Record) { + return api.post("/api/stats", data); +} diff --git a/.benchmark/appointment/frontend/services/users.ts b/.benchmark/appointment/frontend/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/appointment/frontend/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/appointment/frontend/services/ๅฎขๆˆท้€š็Ÿฅ.ts b/.benchmark/appointment/frontend/services/ๅฎขๆˆท้€š็Ÿฅ.ts new file mode 100644 index 0000000..32daa89 --- /dev/null +++ b/.benchmark/appointment/frontend/services/ๅฎขๆˆท้€š็Ÿฅ.ts @@ -0,0 +1,10 @@ +// Auto-generated ๅฎขๆˆท้€š็Ÿฅ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๅฎขๆˆท้€š็Ÿฅ"); +} + +export function post(data: Record) { + return api.post("/api/ๅฎขๆˆท้€š็Ÿฅ", data); +} diff --git a/.benchmark/appointment/frontend/services/ๆ—ถ้—ดๆฎต้ข„็บฆ.ts b/.benchmark/appointment/frontend/services/ๆ—ถ้—ดๆฎต้ข„็บฆ.ts new file mode 100644 index 0000000..277426d --- /dev/null +++ b/.benchmark/appointment/frontend/services/ๆ—ถ้—ดๆฎต้ข„็บฆ.ts @@ -0,0 +1,10 @@ +// Auto-generated ๆ—ถ้—ดๆฎต้ข„็บฆ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๆ—ถ้—ดๆฎต้ข„็บฆ"); +} + +export function post(data: Record) { + return api.post("/api/ๆ—ถ้—ดๆฎต้ข„็บฆ", data); +} diff --git a/.benchmark/appointment/frontend/services/ๆœๅŠก้กน็›ฎ.ts b/.benchmark/appointment/frontend/services/ๆœๅŠก้กน็›ฎ.ts new file mode 100644 index 0000000..10cd0fb --- /dev/null +++ b/.benchmark/appointment/frontend/services/ๆœๅŠก้กน็›ฎ.ts @@ -0,0 +1,10 @@ +// Auto-generated ๆœๅŠก้กน็›ฎ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๆœๅŠก้กน็›ฎ"); +} + +export function post(data: Record) { + return api.post("/api/ๆœๅŠก้กน็›ฎ", data); +} diff --git a/.benchmark/appointment/frontend/services/้ข„็บฆ็ปŸ่ฎก.ts b/.benchmark/appointment/frontend/services/้ข„็บฆ็ปŸ่ฎก.ts new file mode 100644 index 0000000..0206bb0 --- /dev/null +++ b/.benchmark/appointment/frontend/services/้ข„็บฆ็ปŸ่ฎก.ts @@ -0,0 +1,10 @@ +// Auto-generated ้ข„็บฆ็ปŸ่ฎก service +import { api } from "./api"; + +export function get() { + return api.get("/api/้ข„็บฆ็ปŸ่ฎก"); +} + +export function post(data: Record) { + return api.post("/api/้ข„็บฆ็ปŸ่ฎก", data); +} diff --git a/.benchmark/appointment/frontend/tailwind.config.ts b/.benchmark/appointment/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/appointment/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/appointment/frontend/tsconfig.json b/.benchmark/appointment/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/appointment/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/frontend/types/index.ts b/.benchmark/appointment/frontend/types/index.ts new file mode 100644 index 0000000..eeef0b8 --- /dev/null +++ b/.benchmark/appointment/frontend/types/index.ts @@ -0,0 +1,132 @@ +// Auto-generated types for MyProject + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/.gitignore b/.benchmark/appointment/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/appointment/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/appointment/fullstack/README.md b/.benchmark/appointment/fullstack/README.md new file mode 100644 index 0000000..18dd4a3 --- /dev/null +++ b/.benchmark/appointment/fullstack/README.md @@ -0,0 +1,125 @@ +# MyProject โ€” Fullstack Project + +> ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚ + +## Architecture + +``` +apps/ +โ”œโ”€โ”€ web/ # Next.js 15 + TypeScript + Tailwind CSS +โ””โ”€โ”€ api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +โ”œโ”€โ”€ shared-types/ # @shared/types โ€” shared TypeScript interfaces +โ””โ”€โ”€ shared-config/ # @shared/config โ€” shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` โ€” List +- `GET /api/users/:id` โ€” Get +- `POST /api/users` โ€” Create +- `PUT /api/users/:id` โ€” Update +- `DELETE /api/users/:id` โ€” Delete + +#### contracts +- `GET /api/contracts` โ€” List +- `GET /api/contracts/:id` โ€” Get +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List +- `GET /api/approvals/:id` โ€” Get +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List +- `GET /api/customers/:id` โ€” Get +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List +- `GET /api/reminders/:id` โ€” Get +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### services +- `GET /api/services` โ€” List +- `GET /api/services/:id` โ€” Get +- `POST /api/services` โ€” Create +- `PUT /api/services/:id` โ€” Update +- `DELETE /api/services/:id` โ€” Delete + +#### appointments +- `GET /api/appointments` โ€” List +- `GET /api/appointments/:id` โ€” Get +- `POST /api/appointments` โ€” Create +- `PUT /api/appointments/:id` โ€” Update +- `DELETE /api/appointments/:id` โ€” Delete + +#### clients +- `GET /api/clients` โ€” List +- `GET /api/clients/:id` โ€” Get +- `POST /api/clients` โ€” Create +- `PUT /api/clients/:id` โ€” Update +- `DELETE /api/clients/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List +- `GET /api/stats/:id` โ€” Get +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/appointment/fullstack/apps/api/.gitignore b/.benchmark/appointment/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/appointment/fullstack/apps/api/README.md b/.benchmark/appointment/fullstack/apps/api/README.md new file mode 100644 index 0000000..0c5cff6 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/README.md @@ -0,0 +1,118 @@ +# MyProject โ€” Backend API + +> ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### services +- `GET /api/services` โ€” List all +- `GET /api/services/:id` โ€” Get by ID +- `POST /api/services` โ€” Create +- `PUT /api/services/:id` โ€” Update +- `DELETE /api/services/:id` โ€” Delete + +#### appointments +- `GET /api/appointments` โ€” List all +- `GET /api/appointments/:id` โ€” Get by ID +- `POST /api/appointments` โ€” Create +- `PUT /api/appointments/:id` โ€” Update +- `DELETE /api/appointments/:id` โ€” Delete + +#### clients +- `GET /api/clients` โ€” List all +- `GET /api/clients/:id` โ€” Get by ID +- `POST /api/clients` โ€” Create +- `PUT /api/clients/:id` โ€” Update +- `DELETE /api/clients/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List all +- `GET /api/stats/:id` โ€” Get by ID +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/appointment/fullstack/apps/api/package.json b/.benchmark/appointment/fullstack/apps/api/package.json new file mode 100644 index 0000000..2831924 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "myproject-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts b/.benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/appointment/fullstack/apps/api/src/db/client.ts b/.benchmark/appointment/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/db/schema.ts b/.benchmark/appointment/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..ef6555c --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS services (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n description TEXT,\n duration_min INTEGER,\n price REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS appointments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS clients (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n client_id TEXT REFERENCES clients(id),\n service_id TEXT REFERENCES services(id),\n start_time TEXT NOT NULL,\n end_time TEXT,\n status TEXT,\n notes TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/index.ts b/.benchmark/appointment/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..03855d5 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/index.ts @@ -0,0 +1,79 @@ +// MyProject โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { servicesRoutes } from "./routes/services.js"; +import { appointmentsRoutes } from "./routes/appointments.js"; +import { clientsRoutes } from "./routes/clients.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-c69a41fa"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(servicesRoutes, { prefix: "/api/services" }); + await app.register(appointmentsRoutes, { prefix: "/api/appointments" }); + await app.register(clientsRoutes, { prefix: "/api/clients" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/routes/auth.ts b/.benchmark/appointment/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/routes/items.ts b/.benchmark/appointment/fullstack/apps/api/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items โ€” create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/routes/users.ts b/.benchmark/appointment/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/services/item.ts b/.benchmark/appointment/fullstack/apps/api/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/services/user.ts b/.benchmark/appointment/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/types/index.ts b/.benchmark/appointment/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..1677bee --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,223 @@ +// Import and re-export shared entity types +import type { + User, + Contract, + Approval, + Customer, + Reminder, + Services, + Appointments, + Clients, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Contract, + Approval, + Customer, + Reminder, + Services, + Appointments, + Clients, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateServicesInput { + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface CreateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface CreateClientsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + notes?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateServicesInput { + userId?: string; + name?: string; + description?: string; + durationMin?: number; + price?: number; +} + +export interface UpdateAppointmentsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +export interface UpdateClientsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + clientId?: string; + serviceId?: string; + startTime?: string; + endTime?: string; + notes?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/appointment/fullstack/apps/api/tsconfig.json b/.benchmark/appointment/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/web/app/feature/page.tsx b/.benchmark/appointment/fullstack/apps/web/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅŠŸ่ƒฝ้กต

+

ๆ ธๅฟƒๅŠŸ่ƒฝ้กต้ข

+
+ +
+ + { +

ๅŠŸ่ƒฝ้กต ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /feature

+
} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/globals.css b/.benchmark/appointment/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/layout.tsx b/.benchmark/appointment/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..84b9b86 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/page.tsx b/.benchmark/appointment/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..e7a78d1 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ๆœๅŠก้กน็›ฎ + + + ๐Ÿ“… + ๆ—ถ้—ดๆฎต้ข„็บฆ + + + ๐Ÿ“ + ๅฎขๆˆท้€š็Ÿฅ + + + ๐Ÿ“ธ + ้ข„็บฆ็ปŸ่ฎก + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ MyProject

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/app/profile/page.tsx b/.benchmark/appointment/fullstack/apps/web/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆˆ‘็š„

+

็”จๆˆทไธญๅฟƒ

+
+ +
+ + { +

ๆˆ‘็š„ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile

+
} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useForm.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts b/.benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/appointment/fullstack/apps/web/next.config.ts b/.benchmark/appointment/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/appointment/fullstack/apps/web/package.json b/.benchmark/appointment/fullstack/apps/web/package.json new file mode 100644 index 0000000..0e5a224 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/web/postcss.config.js b/.benchmark/appointment/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/appointment/fullstack/apps/web/services/api.ts b/.benchmark/appointment/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..b2e1f85 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for MyProject + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/appointment/fullstack/apps/web/services/health.ts b/.benchmark/appointment/fullstack/apps/web/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/appointment/fullstack/apps/web/services/users.ts b/.benchmark/appointment/fullstack/apps/web/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/appointment/fullstack/apps/web/tailwind.config.ts b/.benchmark/appointment/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/appointment/fullstack/apps/web/tsconfig.json b/.benchmark/appointment/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/apps/web/types/index.ts b/.benchmark/appointment/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..eeef0b8 --- /dev/null +++ b/.benchmark/appointment/fullstack/apps/web/types/index.ts @@ -0,0 +1,132 @@ +// Auto-generated types for MyProject + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/package.json b/.benchmark/appointment/fullstack/package.json new file mode 100644 index 0000000..a32255b --- /dev/null +++ b/.benchmark/appointment/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-config/package.json b/.benchmark/appointment/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-config/src/index.ts b/.benchmark/appointment/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL โ€” reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/appointment/fullstack/packages/shared-config/tsconfig.json b/.benchmark/appointment/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-types/package.json b/.benchmark/appointment/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/packages/shared-types/src/index.ts b/.benchmark/appointment/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..5b2cca5 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,134 @@ +// Shared types for fullstack project +// Auto-generated โ€” used by both apps/web and apps/api + +// โ”€โ”€โ”€ Entities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Services { + id: string; + userId?: string; + name: string; + description?: string; + durationMin?: number; + price?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Appointments { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Clients { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + clientId?: string; + serviceId?: string; + startTime: string; + endTime?: string; + status?: string; + notes?: string; + createdAt?: string; + updatedAt?: string; +} + +// โ”€โ”€โ”€ API Response Wrappers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/appointment/fullstack/packages/shared-types/tsconfig.json b/.benchmark/appointment/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/appointment/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/fullstack/scripts/build.mjs b/.benchmark/appointment/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/appointment/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script โ€” builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n๐Ÿ”จ ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("๐Ÿ—๏ธ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\nโœ… Build complete!"); +} catch (e) { + console.error("\nโŒ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/appointment/fullstack/scripts/dev.mjs b/.benchmark/appointment/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/appointment/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script โ€” starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("๐Ÿš€ Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/appointment/prd.json b/.benchmark/appointment/prd.json new file mode 100644 index 0000000..21997ce --- /dev/null +++ b/.benchmark/appointment/prd.json @@ -0,0 +1 @@ +{"projectName":"MyProject","chineseName":"้€š็”จๅบ”็”จ","domain":"generic","matchConfidence":"low","summary":"ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚","personas":[{"name":"็”จๆˆท","description":"็›ฎๆ ‡็”จๆˆท็พคไฝ“","painPoints":["ๅพ…ๆ˜Ž็กฎ"]}],"userStories":[{"id":"US-001","as":"็”จๆˆท","want":"ๆœๅŠก้กน็›ฎ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅพ…ๆ˜Ž็กฎ","priority":"P0"},{"id":"US-002","as":"็”จๆˆท","want":"ๆ—ถ้—ดๆฎต้ข„็บฆ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅพ…ๆ˜Ž็กฎ","priority":"P0"},{"id":"US-003","as":"็”จๆˆท","want":"ๅฎขๆˆท้€š็Ÿฅ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅพ…ๆ˜Ž็กฎ","priority":"P0"}],"mvpScope":{"description":"MVP ่š็„ฆ ๆœๅŠก้กน็›ฎใ€ๆ—ถ้—ดๆฎต้ข„็บฆใ€ๅฎขๆˆท้€š็Ÿฅใ€้ข„็บฆ็ปŸ่ฎก๏ผŒๅŒ…ๅซ้กต้ข๏ผš้ฆ–้กตใ€ๆœๅŠก้กน็›ฎใ€ๆ—ถ้—ดๆฎต้ข„็บฆ","features":["ๆœๅŠก้กน็›ฎ","ๆ—ถ้—ดๆฎต้ข„็บฆ","ๅฎขๆˆท้€š็Ÿฅ","้ข„็บฆ็ปŸ่ฎก"],"pages":["้ฆ–้กต","ๆœๅŠก้กน็›ฎ","ๆ—ถ้—ดๆฎต้ข„็บฆ"],"estimatedWeeks":3},"features":[{"name":"ๆœๅŠก้กน็›ฎ","description":"ๆœๅŠก้กน็›ฎ","priority":"P0"},{"name":"ๆ—ถ้—ดๆฎต้ข„็บฆ","description":"ๆ—ถ้—ดๆฎต้ข„็บฆ","priority":"P0"},{"name":"ๅฎขๆˆท้€š็Ÿฅ","description":"ๅฎขๆˆท้€š็Ÿฅ","priority":"P0"},{"name":"้ข„็บฆ็ปŸ่ฎก","description":"้ข„็บฆ็ปŸ่ฎก","priority":"P0"}],"pages":[{"name":"้ฆ–้กต","route":"/home","description":"ๅบ”็”จ้ฆ–้กต"},{"name":"ๆœๅŠก้กน็›ฎ","route":"/services","description":"ๆœๅŠก้กน็›ฎ"},{"name":"ๆ—ถ้—ดๆฎต้ข„็บฆ","route":"/appointments","description":"ๆ—ถ้—ดๆฎต้ข„็บฆ"},{"name":"ๅฎขๆˆท้€š็Ÿฅ","route":"/clients","description":"ๅฎขๆˆท้€š็Ÿฅ"},{"name":"้ข„็บฆ็ปŸ่ฎก","route":"/stats","description":"้ข„็บฆ็ปŸ่ฎก"}],"apiRequirements":[{"method":"GET","path":"/api/services","description":"่Žทๅ–ๆœๅŠก้กน็›ฎๅˆ—่กจ"},{"method":"POST","path":"/api/services","description":"ๅˆ›ๅปบๆœๅŠก้กน็›ฎ"},{"method":"GET","path":"/api/appointments","description":"่Žทๅ–ๆ—ถ้—ดๆฎต้ข„็บฆๅˆ—่กจ"},{"method":"POST","path":"/api/appointments","description":"ๅˆ›ๅปบๆ—ถ้—ดๆฎต้ข„็บฆ"},{"method":"GET","path":"/api/clients","description":"่Žทๅ–ๅฎขๆˆท้€š็Ÿฅๅˆ—่กจ"},{"method":"POST","path":"/api/clients","description":"ๅˆ›ๅปบๅฎขๆˆท้€š็Ÿฅ"},{"method":"GET","path":"/api/stats","description":"่Žทๅ–้ข„็บฆ็ปŸ่ฎกๅˆ—่กจ"},{"method":"POST","path":"/api/stats","description":"ๅˆ›ๅปบ้ข„็บฆ็ปŸ่ฎก"},{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter๏ผˆ่ทจๅนณๅฐ๏ผ‰","considerations":["ๅปบ่ฎฎ่ทจๅนณๅฐๆก†ๆžถๅ‡ๅฐ‘ๅผ€ๅ‘ๆˆๆœฌ","้œ€ๅฏนๆŽฅๆŽจ้€ๆœๅŠก๏ผˆAPNs / FCM / ไธชๆŽจ๏ผ‰"]},"extraFeatures":["push-notification"],"devTasks":[{"id":"T-001","title":"้กน็›ฎ่„šๆ‰‹ๆžถๆญๅปบ","description":"ๅˆๅง‹ๅŒ– MyProject ้กน็›ฎ็ป“ๆž„๏ผŒ้…็ฝฎๆž„ๅปบๅทฅๅ…ทๅ’Œ CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"ๆ•ฐๆฎๅบ“่ฎพ่ฎกไธŽๅˆๅง‹ๅŒ–","description":"่ฎพ่ฎกๆ•ฐๆฎๆจกๅž‹๏ผŒๅˆ›ๅปบๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌ","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"้กต้ขๅผ€ๅ‘๏ผš้ฆ–้กต","description":"ๅผ€ๅ‘ ้ฆ–้กต ้กต้ข๏ผˆ/home๏ผ‰๏ผšๅบ”็”จ้ฆ–้กต","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"้กต้ขๅผ€ๅ‘๏ผšๆœๅŠก้กน็›ฎ","description":"ๅผ€ๅ‘ ๆœๅŠก้กน็›ฎ ้กต้ข๏ผˆ/services๏ผ‰๏ผšๆœๅŠก้กน็›ฎ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"้กต้ขๅผ€ๅ‘๏ผšๆ—ถ้—ดๆฎต้ข„็บฆ","description":"ๅผ€ๅ‘ ๆ—ถ้—ดๆฎต้ข„็บฆ ้กต้ข๏ผˆ/appointments๏ผ‰๏ผšๆ—ถ้—ดๆฎต้ข„็บฆ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"้กต้ขๅผ€ๅ‘๏ผšๅฎขๆˆท้€š็Ÿฅ","description":"ๅผ€ๅ‘ ๅฎขๆˆท้€š็Ÿฅ ้กต้ข๏ผˆ/clients๏ผ‰๏ผšๅฎขๆˆท้€š็Ÿฅ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API ๅผ€ๅ‘๏ผšGET /api/services","description":"่Žทๅ–ๆœๅŠก้กน็›ฎๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API ๅผ€ๅ‘๏ผšPOST /api/services","description":"ๅˆ›ๅปบๆœๅŠก้กน็›ฎ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API ๅผ€ๅ‘๏ผšGET /api/appointments","description":"่Žทๅ–ๆ—ถ้—ดๆฎต้ข„็บฆๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API ๅผ€ๅ‘๏ผšPOST /api/appointments","description":"ๅˆ›ๅปบๆ—ถ้—ดๆฎต้ข„็บฆ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"ๅ‰ๅŽ็ซฏ่”่ฐƒ","description":"ๅ‰็ซฏ้กต้ขไธŽๅŽ็ซฏ API ้›†ๆˆ่”่ฐƒ","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"ๆต‹่ฏ•ไธŽไฟฎๅค","description":"ๅŠŸ่ƒฝๆต‹่ฏ•ใ€Bug ไฟฎๅคใ€ๆ€ง่ƒฝไผ˜ๅŒ–","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:37.795Z","inputLength":32,"domainMatchCount":0,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/README.md b/.benchmark/appointment/release/release/README.md new file mode 100644 index 0000000..e41b6d8 --- /dev/null +++ b/.benchmark/appointment/release/release/README.md @@ -0,0 +1,33 @@ +# myproject โ€” Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:38.148Z + +## Directory Structure + +``` +release/ +โ”œโ”€โ”€ version.json โ€” Version metadata +โ”œโ”€โ”€ build-info.json โ€” Build environment info +โ”œโ”€โ”€ README.md โ€” This file +โ”œโ”€โ”€ manifests/ +โ”‚ โ””โ”€โ”€ manifest.json โ€” Release manifest with file hashes +โ”œโ”€โ”€ checksums/ +โ”‚ โ””โ”€โ”€ checksums.txt โ€” SHA256 checksums for verification +โ”œโ”€โ”€ release-notes/ +โ”‚ โ””โ”€โ”€ release-notes.md โ€” Human-readable release notes +โ”œโ”€โ”€ windows/ โ€” Windows installers +โ”œโ”€โ”€ macos/ โ€” macOS disk images +โ””โ”€โ”€ linux/ โ€” Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/appointment/release/release/build-info.json b/.benchmark/appointment/release/release/build-info.json new file mode 100644 index 0000000..d15a6b8 --- /dev/null +++ b/.benchmark/appointment/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:38.148Z", + "domain": "unknown", + "entityCount": 11, + "routeCount": 1, + "screenCount": 10, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "myproject", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/checksums/checksums.txt b/.benchmark/appointment/release/release/checksums/checksums.txt new file mode 100644 index 0000000..4829db1 --- /dev/null +++ b/.benchmark/appointment/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +2655f13daf610abdd377aaecf56a7e5998b154d3fc7796c5c7c79ccec520fb81 windows/myproject-setup-0.1.0.exe +f67e8b1cf8423a05fa9e50cc34301b92599c85b5c3db83c5203b6afad6ab219b windows/myproject-0.1.0-portable.exe +1a44d99066ab124ad4662f5b1b1097633670eb2546effdaad54cc2b56517c7c4 macos/myproject-0.1.0.dmg +6fa1c4cca0c3760eeebe9824e66eeee02847430194a8daf68797279c2d11ab81 macos/myproject-0.1.0-arm64.dmg +fbc3f5117a66cb12131b35a5c8cf60c0615cc31cbc4a5a46c58a3e244b77369f linux/myproject-0.1.0.AppImage +8412b2ffc545a1036a7a60f0207e4345293ed3a7c2c5a81a5cd94798e0302f9e linux/myproject_0.1.0_amd64.deb diff --git a/.benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage b/.benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage new file mode 100644 index 0000000..17ff5b8 --- /dev/null +++ b/.benchmark/appointment/release/release/linux/myproject-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb b/.benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb new file mode 100644 index 0000000..82f71b2 --- /dev/null +++ b/.benchmark/appointment/release/release/linux/myproject_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg b/.benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg new file mode 100644 index 0000000..b3f27bc --- /dev/null +++ b/.benchmark/appointment/release/release/macos/myproject-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/macos/myproject-0.1.0.dmg b/.benchmark/appointment/release/release/macos/myproject-0.1.0.dmg new file mode 100644 index 0000000..5c61779 --- /dev/null +++ b/.benchmark/appointment/release/release/macos/myproject-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/manifests/manifest.json b/.benchmark/appointment/release/release/manifests/manifest.json new file mode 100644 index 0000000..77aabfc --- /dev/null +++ b/.benchmark/appointment/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:38.148Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/myproject-setup-0.1.0.exe", + "size": 144, + "sha256": "2655f13daf610abdd377aaecf56a7e5998b154d3fc7796c5c7c79ccec520fb81" + }, + { + "path": "windows/myproject-0.1.0-portable.exe", + "size": 148, + "sha256": "f67e8b1cf8423a05fa9e50cc34301b92599c85b5c3db83c5203b6afad6ab219b" + }, + { + "path": "macos/myproject-0.1.0.dmg", + "size": 140, + "sha256": "1a44d99066ab124ad4662f5b1b1097633670eb2546effdaad54cc2b56517c7c4" + }, + { + "path": "macos/myproject-0.1.0-arm64.dmg", + "size": 142, + "sha256": "6fa1c4cca0c3760eeebe9824e66eeee02847430194a8daf68797279c2d11ab81" + }, + { + "path": "linux/myproject-0.1.0.AppImage", + "size": 133, + "sha256": "fbc3f5117a66cb12131b35a5c8cf60c0615cc31cbc4a5a46c58a3e244b77369f" + }, + { + "path": "linux/myproject_0.1.0_amd64.deb", + "size": 143, + "sha256": "8412b2ffc545a1036a7a60f0207e4345293ed3a7c2c5a81a5cd94798e0302f9e" + } + ], + "totalFiles": 6, + "totalSize": 850 +} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/release-notes/release-notes.md b/.benchmark/appointment/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..10715a8 --- /dev/null +++ b/.benchmark/appointment/release/release/release-notes/release-notes.md @@ -0,0 +1,60 @@ +# myproject v0.1.0 + +> Release Builder Agent โ€” SF-07 +> Generated: 2026-06-05 22:08:38 UTC + +## Version + +- **Name:** myproject +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- appointments +- approvals +- auth +- clients +- contracts +- customers +- items +- reminders +- services +- stats +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:38.148Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- โœ… windows +- โœ… macos +- โœ… linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/appointment/release/release/version.json b/.benchmark/appointment/release/release/version.json new file mode 100644 index 0000000..3a7e2b5 --- /dev/null +++ b/.benchmark/appointment/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe b/.benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe new file mode 100644 index 0000000..077132d --- /dev/null +++ b/.benchmark/appointment/release/release/windows/myproject-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe b/.benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe new file mode 100644 index 0000000..4bbe350 --- /dev/null +++ b/.benchmark/appointment/release/release/windows/myproject-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:38.147Z diff --git a/.benchmark/asset/arch.json b/.benchmark/asset/arch.json new file mode 100644 index 0000000..9e2b44f --- /dev/null +++ b/.benchmark/asset/arch.json @@ -0,0 +1 @@ +{"projectName":"MyProject","domain":"generic","contract":{"projectName":"MyProject","domain":"generic","entities":[{"name":"User","table":"users","description":"็ณป็ปŸ็”จๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"ๅˆๅŒ","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"ๅฎกๆ‰นๆต็จ‹","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"ๅฎขๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"ๅˆฐๆœŸๆ้†’","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Assets","table":"assets","description":"่ต„ไบง็™ป่ฎฐ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"category","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"purchase_date","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"purchase_price","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"current_value","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"location","type":"VARCHAR(128)","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Items","table":"items","description":"้ข†็”จๅฝ’่ฟ˜่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"data","type":"JSONB","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Stats","table":"stats","description":"็›˜็‚น็ปŸ่ฎก่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"metric","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false},{"name":"value","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"period","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":false},{"name":"recorded_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","purchase_date":"purchaseDate","purchase_price":"purchasePrice","current_value":"currentValue","recorded_at":"recordedAt","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","purchaseDate":"purchase_date","purchasePrice":"purchase_price","currentValue":"current_value","recordedAt":"recorded_at","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Assets":{"id":{"required":true},"name":{"required":true}},"Items":{"id":{"required":true},"title":{"required":true}},"Stats":{"id":{"required":true},"metric":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO๏ผˆๆ–‡ไปถๅญ˜ๅ‚จ๏ผ‰","deployment":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","considerations":[]},"architectureDiagram":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ MyProject โ€” System Architecture โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Client Layer โ”‚ โ”‚ Admin / Web โ”‚ โ”‚\nโ”‚ โ”‚ React Native 0.76 + Expo / Flutter 3.x โ”‚ โ”‚ React + Vite (Web) โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ API Gateway โ”‚ โ”‚\nโ”‚ โ”‚ Nginx / Kong โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Backend Services โ”‚ โ”‚\nโ”‚ โ”‚ NestJS + Prisma โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ PostgreSQL 15โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ MinIO โ”‚ โ”‚\nโ”‚ โ”‚ Primary โ”‚ โ”‚ Session โ”‚ โ”‚ Files โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ Modules: โ”‚\nโ”‚ 1 ่ต„ไบง็™ป่ฎฐ โ”‚\nโ”‚ 2 ้ข†็”จๅฝ’่ฟ˜ โ”‚\nโ”‚ 3 ๆŠ˜ๆ—ง่ฎก็ฎ— โ”‚\nโ”‚ 4 Analytics โ”‚\nโ”‚ โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","dataFlows":[{"page":"่ต„ไบง็™ป่ฎฐ","route":"/assets","description":"่ต„ไบง็™ป่ฎฐ","dataFlow":["GET /api/assets โ†’ ่Žทๅ–่ต„ไบง็™ป่ฎฐๅˆ—่กจ","POST /api/assets โ†’ ๅˆ›ๅปบ่ต„ไบง็™ป่ฎฐ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"้ข†็”จๅฝ’่ฟ˜","route":"/items","description":"้ข†็”จๅฝ’่ฟ˜","dataFlow":["GET /api/items โ†’ ่Žทๅ–้ข†็”จๅฝ’่ฟ˜ๅˆ—่กจ","POST /api/items โ†’ ๅˆ›ๅปบ้ข†็”จๅฝ’่ฟ˜"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๆŠ˜ๆ—ง่ฎก็ฎ—","route":"/items","description":"ๆŠ˜ๆ—ง่ฎก็ฎ—","dataFlow":["GET /api/items โ†’ ่Žทๅ–้ข†็”จๅฝ’่ฟ˜ๅˆ—่กจ","POST /api/items โ†’ ๅˆ›ๅปบ้ข†็”จๅฝ’่ฟ˜"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"็›˜็‚น็ปŸ่ฎก","route":"/stats","description":"็›˜็‚น็ปŸ่ฎก","dataFlow":["GET /api/stats โ†’ ่Žทๅ–็›˜็‚น็ปŸ่ฎกๅˆ—่กจ","POST /api/stats โ†’ ๅˆ›ๅปบ็›˜็‚น็ปŸ่ฎก"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"}],"modules":[{"name":"่ต„ไบง็™ป่ฎฐ","label":"่ต„ไบง็™ป่ฎฐ","features":["่ต„ไบง็™ป่ฎฐ"],"responsibilities":["ๆไพ› ่ต„ไบง็™ป่ฎฐ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"้ข†็”จๅฝ’่ฟ˜","label":"้ข†็”จๅฝ’่ฟ˜","features":["้ข†็”จๅฝ’่ฟ˜"],"responsibilities":["ๆไพ› ้ข†็”จๅฝ’่ฟ˜ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ๆŠ˜ๆ—ง่ฎก็ฎ—","label":"ๆŠ˜ๆ—ง่ฎก็ฎ—","features":["ๆŠ˜ๆ—ง่ฎก็ฎ—"],"responsibilities":["ๆไพ› ๆŠ˜ๆ—ง่ฎก็ฎ— ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"analytics","label":"Analytics","features":["็›˜็‚น็ปŸ่ฎก"],"responsibilities":["ๆไพ› ็›˜็‚น็ปŸ่ฎก ็›ธๅ…ณๅŠŸ่ƒฝ"]}],"databaseSchema":[{"table":"users","description":"็”จๆˆท่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"assets","description":"่ต„ไบง็™ป่ฎฐ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"category","type":"VARCHAR(64)"},{"name":"purchase_date","type":"DATE"},{"name":"purchase_price","type":"DECIMAL(12,2)"},{"name":"current_value","type":"DECIMAL(12,2)"},{"name":"location","type":"VARCHAR(128)"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'available'"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_assets_user ON assets(user_id)"]},{"table":"items","description":"้ข†็”จๅฝ’่ฟ˜่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'active'"},{"name":"data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_items_user ON items(user_id)"]},{"table":"stats","description":"็›˜็‚น็ปŸ่ฎก่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"metric","type":"VARCHAR(64)","constraints":"NOT NULL"},{"name":"value","type":"DECIMAL(12,2)"},{"name":"period","type":"VARCHAR(32)"},{"name":"recorded_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_stats_user ON stats(user_id)"]}],"apiDesign":[{"resource":"assets","basePath":"/api/assets","endpoints":[{"method":"GET","path":"/api/assets","description":"่Žทๅ–่ต„ไบง็™ป่ฎฐๅˆ—่กจ"},{"method":"POST","path":"/api/assets","description":"ๅˆ›ๅปบ่ต„ไบง็™ป่ฎฐ"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}]},{"resource":"items","basePath":"/api/items","endpoints":[{"method":"GET","path":"/api/items","description":"่Žทๅ–้ข†็”จๅฝ’่ฟ˜ๅˆ—่กจ"},{"method":"POST","path":"/api/items","description":"ๅˆ›ๅปบ้ข†็”จๅฝ’่ฟ˜"},{"method":"GET","path":"/api/items","description":"่Žทๅ–ๆŠ˜ๆ—ง่ฎก็ฎ—ๅˆ—่กจ"},{"method":"POST","path":"/api/items","description":"ๅˆ›ๅปบๆŠ˜ๆ—ง่ฎก็ฎ—"}]},{"resource":"stats","basePath":"/api/stats","endpoints":[{"method":"GET","path":"/api/stats","description":"่Žทๅ–็›˜็‚น็ปŸ่ฎกๅˆ—่กจ"},{"method":"POST","path":"/api/stats","description":"ๅˆ›ๅปบ็›˜็‚น็ปŸ่ฎก"}]}],"directoryStructure":["myproject/","โ”œโ”€โ”€ apps/","โ”‚ โ”œโ”€โ”€ mobile/ # React Native / Flutter ็งปๅŠจ็ซฏ","โ”‚ โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # ้กต้ข็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ต„ไบง็™ป่ฎฐ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้ข†็”จๅฝ’่ฟ˜/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆŠ˜ๆ—ง่ฎก็ฎ—/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ้€š็”จ็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # ่‡ชๅฎšไน‰ Hooks","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API ่ฐƒ็”จๅฑ‚","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ # ็Šถๆ€็ฎก็†","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ","โ”‚ โ”‚ โ”œโ”€โ”€ app.json","โ”‚ โ”‚ โ””โ”€โ”€ package.json","โ”‚ โ””โ”€โ”€ web/ # Web ็ฎก็†ๅŽๅฐ","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ pages/","โ”‚ โ”‚ โ”œโ”€โ”€ components/","โ”‚ โ”‚ โ””โ”€โ”€ layouts/","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ packages/","โ”‚ โ””โ”€โ”€ shared/ # ๅ…ฑไบซ็ฑปๅž‹/ๅธธ้‡","โ”œโ”€โ”€ server/ # NestJS ๅŽ็ซฏๆœๅŠก","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ modules/ # ไธšๅŠกๆจกๅ—","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ต„ไบง็™ป่ฎฐ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ต„ไบง็™ป่ฎฐ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ต„ไบง็™ป่ฎฐ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ต„ไบง็™ป่ฎฐ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้ข†็”จๅฝ’่ฟ˜/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้ข†็”จๅฝ’่ฟ˜.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้ข†็”จๅฝ’่ฟ˜.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้ข†็”จๅฝ’่ฟ˜.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆŠ˜ๆ—ง่ฎก็ฎ—/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆŠ˜ๆ—ง่ฎก็ฎ—.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆŠ˜ๆ—ง่ฎก็ฎ—.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆŠ˜ๆ—ง่ฎก็ฎ—.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”œโ”€โ”€ common/ # ้€š็”จ๏ผˆguards, filters, interceptors๏ผ‰","โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ # Prisma ORM","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ schema.prisma","โ”‚ โ”‚ โ”œโ”€โ”€ config/ # ็Žฏๅขƒ้…็ฝฎ","โ”‚ โ”‚ โ””โ”€โ”€ main.ts","โ”‚ โ”œโ”€โ”€ test/","โ”‚ โ”œโ”€โ”€ Dockerfile","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ docker-compose.yml","โ”œโ”€โ”€ .github/workflows/ # CI/CD","โ”‚ โ””โ”€โ”€ ci.yml","โ”œโ”€โ”€ .env.example","โ”œโ”€โ”€ README.md","โ””โ”€โ”€ turbo.json # Monorepo ้…็ฝฎ"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions โ†’ Build โ†’ Test โ†’ Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:37.042Z","sourceDomain":"generic","moduleCount":4,"tableCount":4,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/asset/backend/.gitignore b/.benchmark/asset/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/asset/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/asset/backend/README.md b/.benchmark/asset/backend/README.md new file mode 100644 index 0000000..8983604 --- /dev/null +++ b/.benchmark/asset/backend/README.md @@ -0,0 +1,111 @@ +# MyProject โ€” Backend API + +> ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### assets +- `GET /api/assets` โ€” List all +- `GET /api/assets/:id` โ€” Get by ID +- `POST /api/assets` โ€” Create +- `PUT /api/assets/:id` โ€” Update +- `DELETE /api/assets/:id` โ€” Delete + +#### items +- `GET /api/items` โ€” List all +- `GET /api/items/:id` โ€” Get by ID +- `POST /api/items` โ€” Create +- `PUT /api/items/:id` โ€” Update +- `DELETE /api/items/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List all +- `GET /api/stats/:id` โ€” Get by ID +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/asset/backend/package.json b/.benchmark/asset/backend/package.json new file mode 100644 index 0000000..e3106a0 --- /dev/null +++ b/.benchmark/asset/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/backend/src/__tests__/approvals.test.ts b/.benchmark/asset/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/assets.test.ts b/.benchmark/asset/backend/src/__tests__/assets.test.ts new file mode 100644 index 0000000..5e3205f --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/assets.test.ts @@ -0,0 +1,122 @@ +// Assets CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "category": "sample-category", + "purchaseDate": "sample-purchasedate", + "purchasePrice": 1, + "currentValue": 1, + "location": "sample-location" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/assets", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/assets", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/assets", () => { + it("creates a asset", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/assets", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/assets/:id", () => { + it("returns the created asset", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assets/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/assets/:id", () => { + it("updates the asset", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/assets/:id", () => { + it("deletes the asset", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assets/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/auth.test.ts b/.benchmark/asset/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/contracts.test.ts b/.benchmark/asset/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/customers.test.ts b/.benchmark/asset/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/items.test.ts b/.benchmark/asset/backend/src/__tests__/items.test.ts new file mode 100644 index 0000000..ff42fbb --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/items.test.ts @@ -0,0 +1,119 @@ +// Items CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/reminders.test.ts b/.benchmark/asset/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/stats.test.ts b/.benchmark/asset/backend/src/__tests__/stats.test.ts new file mode 100644 index 0000000..1b64b6d --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/stats.test.ts @@ -0,0 +1,119 @@ +// Stats CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "metric": "sample-metric", + "value": 1, + "period": "sample-period" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/stats", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/stats", () => { + it("creates a stat", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/stats/:id", () => { + it("returns the created stat", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/stats/:id", () => { + it("updates the stat", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/stats/:id", () => { + it("deletes the stat", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/__tests__/users.test.ts b/.benchmark/asset/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/asset/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/backend/src/db/client.ts b/.benchmark/asset/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/asset/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/asset/backend/src/db/schema.ts b/.benchmark/asset/backend/src/db/schema.ts new file mode 100644 index 0000000..425ac3a --- /dev/null +++ b/.benchmark/asset/backend/src/db/schema.ts @@ -0,0 +1,19 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assets (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n category TEXT,\n purchase_date TEXT,\n purchase_price REAL,\n current_value REAL,\n location TEXT,\n status TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS items (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n metric TEXT NOT NULL,\n value REAL,\n period TEXT,\n recorded_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/asset/backend/src/index.ts b/.benchmark/asset/backend/src/index.ts new file mode 100644 index 0000000..a72a7fc --- /dev/null +++ b/.benchmark/asset/backend/src/index.ts @@ -0,0 +1,77 @@ +// MyProject โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { assetsRoutes } from "./routes/assets.js"; +import { itemsRoutes } from "./routes/items.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-a91d83e4"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(assetsRoutes, { prefix: "/api/assets" }); + await app.register(itemsRoutes, { prefix: "/api/items" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/asset/backend/src/middleware/auth.ts b/.benchmark/asset/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/asset/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/asset/backend/src/routes/approvals.ts b/.benchmark/asset/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals โ€” create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/assets.ts b/.benchmark/asset/backend/src/routes/assets.ts new file mode 100644 index 0000000..7c19598 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/assets.ts @@ -0,0 +1,51 @@ +// Auto-generated Assets routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { AssetsService } from "../services/asset.js"; +import type { CreateAssetsInput, UpdateAssetsInput } from "../types/index.js"; + +const service = new AssetsService(); + +export async function assetsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/assets โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/assets/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assets not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/assets โ€” create + app.post<{ Body: CreateAssetsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/assets/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateAssetsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assets not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/assets/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Assets not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/auth.ts b/.benchmark/asset/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/asset/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/asset/backend/src/routes/contracts.ts b/.benchmark/asset/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/asset/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts โ€” create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/customers.ts b/.benchmark/asset/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers โ€” create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/items.ts b/.benchmark/asset/backend/src/routes/items.ts new file mode 100644 index 0000000..a649fff --- /dev/null +++ b/.benchmark/asset/backend/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Items routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemsService } from "../services/item.js"; +import type { CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +const service = new ItemsService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items โ€” create + app.post<{ Body: CreateItemsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateItemsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/reminders.ts b/.benchmark/asset/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders โ€” create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/stats.ts b/.benchmark/asset/backend/src/routes/stats.ts new file mode 100644 index 0000000..92f64ea --- /dev/null +++ b/.benchmark/asset/backend/src/routes/stats.ts @@ -0,0 +1,51 @@ +// Auto-generated Stats routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StatsService } from "../services/stat.js"; +import type { CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +const service = new StatsService(); + +export async function statsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/stats โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/stats/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/stats โ€” create + app.post<{ Body: CreateStatsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/stats/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateStatsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/stats/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/routes/users.ts b/.benchmark/asset/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/asset/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/backend/src/services/approval.ts b/.benchmark/asset/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/asset/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/asset.ts b/.benchmark/asset/backend/src/services/asset.ts new file mode 100644 index 0000000..4727aea --- /dev/null +++ b/.benchmark/asset/backend/src/services/asset.ts @@ -0,0 +1,57 @@ +// Auto-generated Assets service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Assets, CreateAssetsInput, UpdateAssetsInput } from "../types/index.js"; + +export class AssetsService { + /** List all assets */ + list(): Assets[] { + return queryAll("SELECT * FROM assets ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Assets | undefined { + return queryOne("SELECT * FROM assets WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateAssetsInput): Assets { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "category", "purchase_date", "purchase_price", "current_value", "location", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.category ?? null, input.purchaseDate ?? null, input.purchasePrice ?? null, input.currentValue ?? null, input.location ?? null, now, now]; + + execute(`INSERT INTO assets (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateAssetsInput): Assets | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + if (input.purchaseDate !== undefined) { sets.push("purchase_date = ?"); values.push(input.purchaseDate); } + if (input.purchasePrice !== undefined) { sets.push("purchase_price = ?"); values.push(input.purchasePrice); } + if (input.currentValue !== undefined) { sets.push("current_value = ?"); values.push(input.currentValue); } + if (input.location !== undefined) { sets.push("location = ?"); values.push(input.location); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE assets SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM assets WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/contract.ts b/.benchmark/asset/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/asset/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/customer.ts b/.benchmark/asset/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/asset/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/item.ts b/.benchmark/asset/backend/src/services/item.ts new file mode 100644 index 0000000..6ec5f12 --- /dev/null +++ b/.benchmark/asset/backend/src/services/item.ts @@ -0,0 +1,54 @@ +// Auto-generated Items service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Items, CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +export class ItemsService { + /** List all items */ + list(): Items[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Items | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemsInput): Items { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemsInput): Items | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/reminder.ts b/.benchmark/asset/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/asset/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/stat.ts b/.benchmark/asset/backend/src/services/stat.ts new file mode 100644 index 0000000..1d4080d --- /dev/null +++ b/.benchmark/asset/backend/src/services/stat.ts @@ -0,0 +1,54 @@ +// Auto-generated Stats service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Stats, CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +export class StatsService { + /** List all stats */ + list(): Stats[] { + return queryAll("SELECT * FROM stats ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Stats | undefined { + return queryOne("SELECT * FROM stats WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStatsInput): Stats { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "metric", "value", "period", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.metric ?? null, input.value ?? null, input.period ?? null, now, now]; + + execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStatsInput): Stats | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.metric !== undefined) { sets.push("metric = ?"); values.push(input.metric); } + if (input.value !== undefined) { sets.push("value = ?"); values.push(input.value); } + if (input.period !== undefined) { sets.push("period = ?"); values.push(input.period); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM stats WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/services/user.ts b/.benchmark/asset/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/asset/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/backend/src/types/fastify.d.ts b/.benchmark/asset/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/asset/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/asset/backend/src/types/index.ts b/.benchmark/asset/backend/src/types/index.ts new file mode 100644 index 0000000..e5650b7 --- /dev/null +++ b/.benchmark/asset/backend/src/types/index.ts @@ -0,0 +1,292 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateAssetsInput { + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface CreateItemsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + metric: string; + value?: number; + period?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateAssetsInput { + userId?: string; + name?: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface UpdateItemsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + metric?: string; + value?: number; + period?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/asset/backend/src/types/sql.js.d.ts b/.benchmark/asset/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/asset/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/asset/backend/tsconfig.json b/.benchmark/asset/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/asset/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/electron/README.md b/.benchmark/asset/electron/README.md new file mode 100644 index 0000000..cb32bc8 --- /dev/null +++ b/.benchmark/asset/electron/README.md @@ -0,0 +1,82 @@ +# myproject โ€” Desktop + +> Electron desktop application wrapping the myproject fullstack web project. + +## Architecture + +``` +Web Fullstack Project + ร— + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** โ€” Window management, menu, tray, IPC, auto-update +- **Preload** โ€” Secure bridge between main and renderer +- **Renderer** โ€” Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +โ”œโ”€โ”€ main.ts โ€” Main process (BrowserWindow, Menu, lifecycle) +โ”œโ”€โ”€ preload.ts โ€” Secure IPC bridge (contextBridge) +โ”œโ”€โ”€ ipc.ts โ€” IPC handlers (dialogs, store, notifications) +โ”œโ”€โ”€ tray.ts โ€” System tray +โ”œโ”€โ”€ updater.ts โ€” Auto-update (electron-updater) +โ””โ”€โ”€ utils.ts โ€” Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/asset/electron/assets/icon.png b/.benchmark/asset/electron/assets/icon.png new file mode 100644 index 0000000..554e8a3 Binary files /dev/null and b/.benchmark/asset/electron/assets/icon.png differ diff --git a/.benchmark/asset/electron/electron-builder.yml b/.benchmark/asset/electron/electron-builder.yml new file mode 100644 index 0000000..1ab271c --- /dev/null +++ b/.benchmark/asset/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml โ€” Auto-generated by SF-06 + +appId: com.myproject.desktop +productName: myproject +copyright: "Copyright ยฉ 2026 myproject" + +# โ”€โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +directories: + output: release + buildResources: assets + +# โ”€โ”€โ”€ Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# โ”€โ”€โ”€ Extra Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# โ”€โ”€โ”€ macOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# โ”€โ”€โ”€ Windows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: myproject + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: myproject + +# โ”€โ”€โ”€ Linux โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: myproject + Comment: myproject Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# โ”€โ”€โ”€ Auto Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +publish: + provider: generic + url: https://releases.example.com/myproject/ + channel: latest diff --git a/.benchmark/asset/electron/electron/ipc.ts b/.benchmark/asset/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/asset/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // โ”€โ”€ App Info โ”€โ”€ + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // โ”€โ”€ Window Controls โ”€โ”€ + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // โ”€โ”€ File Dialogs โ”€โ”€ + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // โ”€โ”€ Notifications โ”€โ”€ + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // โ”€โ”€ Shell โ”€โ”€ + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // โ”€โ”€ Persistent Store โ”€โ”€ + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/asset/electron/electron/main.ts b/.benchmark/asset/electron/electron/main.ts new file mode 100644 index 0000000..eaab114 --- /dev/null +++ b/.benchmark/asset/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "myproject"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// โ”€โ”€โ”€ Create Window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // โ”€โ”€ Load content โ”€โ”€ + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // โ”€โ”€ Show when ready โ”€โ”€ + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // โ”€โ”€ External links โ†’ default browser โ”€โ”€ + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // โ”€โ”€ Prevent navigation away โ”€โ”€ + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// โ”€โ”€โ”€ Icon Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// โ”€โ”€โ”€ API Server (Production) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// โ”€โ”€โ”€ Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// โ”€โ”€โ”€ App Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed โ€” continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/asset/electron/electron/preload.ts b/.benchmark/asset/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/asset/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// โ”€โ”€โ”€ Expose safe API to renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +contextBridge.exposeInMainWorld("electronAPI", { + // โ”€โ”€ App Info โ”€โ”€ + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // โ”€โ”€ Window Controls โ”€โ”€ + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // โ”€โ”€ File Dialogs โ”€โ”€ + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // โ”€โ”€ Notifications โ”€โ”€ + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // โ”€โ”€ Shell โ”€โ”€ + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // โ”€โ”€ Store โ”€โ”€ + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // โ”€โ”€ Updates โ”€โ”€ + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // โ”€โ”€ Update Events โ”€โ”€ + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // โ”€โ”€ Remove listener โ”€โ”€ + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// โ”€โ”€โ”€ Type declarations for renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/asset/electron/electron/tray.ts b/.benchmark/asset/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/asset/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/asset/electron/electron/updater.ts b/.benchmark/asset/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/asset/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/asset/electron/electron/utils.ts b/.benchmark/asset/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/asset/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// โ”€โ”€โ”€ Dev mode detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function isDev(): boolean { + return !app.isPackaged; +} + +// โ”€โ”€โ”€ Web URL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// โ”€โ”€โ”€ API Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// โ”€โ”€โ”€ Get API port from env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/asset/electron/entitlements.mac.plist b/.benchmark/asset/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/asset/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/asset/electron/package.json b/.benchmark/asset/electron/package.json new file mode 100644 index 0000000..44bc859 --- /dev/null +++ b/.benchmark/asset/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "myproject-desktop", + "version": "1.0.0", + "private": true, + "description": "myproject โ€” Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "myproject", + "appId": "com.myproject.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/asset/electron/tsconfig.json b/.benchmark/asset/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/asset/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/frontend/app/assets/page.tsx b/.benchmark/asset/frontend/app/assets/page.tsx new file mode 100644 index 0000000..6d60dc2 --- /dev/null +++ b/.benchmark/asset/frontend/app/assets/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useAssets } from "@/hooks/useAssets"; + +export default function PagePage() { + const { data, loading, error, refetch } = useAssets(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

่ต„ไบง็™ป่ฎฐ

+

่ต„ไบง็™ป่ฎฐ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/feature/page.tsx b/.benchmark/asset/frontend/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/asset/frontend/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅŠŸ่ƒฝ้กต

+

ๆ ธๅฟƒๅŠŸ่ƒฝ้กต้ข

+
+ +
+ + { +

ๅŠŸ่ƒฝ้กต ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /feature

+
} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/globals.css b/.benchmark/asset/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/asset/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/asset/frontend/app/items/page.tsx b/.benchmark/asset/frontend/app/items/page.tsx new file mode 100644 index 0000000..808e453 --- /dev/null +++ b/.benchmark/asset/frontend/app/items/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useItems } from "@/hooks/useItems"; + +export default function PagePage() { + const { data, loading, error, refetch } = useItems(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆŠ˜ๆ—ง่ฎก็ฎ—

+

ๆŠ˜ๆ—ง่ฎก็ฎ—

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/layout.tsx b/.benchmark/asset/frontend/app/layout.tsx new file mode 100644 index 0000000..43f1cb9 --- /dev/null +++ b/.benchmark/asset/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/asset/frontend/app/page.tsx b/.benchmark/asset/frontend/app/page.tsx new file mode 100644 index 0000000..203f24f --- /dev/null +++ b/.benchmark/asset/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ่ต„ไบง็™ป่ฎฐ + + + ๐Ÿ“… + ้ข†็”จๅฝ’่ฟ˜ + + + ๐Ÿ“ + ๆŠ˜ๆ—ง่ฎก็ฎ— + + + ๐Ÿ“ธ + ็›˜็‚น็ปŸ่ฎก + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ MyProject

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/asset/frontend/app/profile/page.tsx b/.benchmark/asset/frontend/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/asset/frontend/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆˆ‘็š„

+

็”จๆˆทไธญๅฟƒ

+
+ +
+ + { +

ๆˆ‘็š„ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile

+
} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/stats/page.tsx b/.benchmark/asset/frontend/app/stats/page.tsx new file mode 100644 index 0000000..a11cc1e --- /dev/null +++ b/.benchmark/asset/frontend/app/stats/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useStats } from "@/hooks/useStats"; + +export default function PagePage() { + const { data, loading, error, refetch } = useStats(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

็›˜็‚น็ปŸ่ฎก

+

็›˜็‚น็ปŸ่ฎก

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/ๆŠ˜ๆ—ง่ฎก็ฎ—/page.tsx b/.benchmark/asset/frontend/app/ๆŠ˜ๆ—ง่ฎก็ฎ—/page.tsx new file mode 100644 index 0000000..1e72994 --- /dev/null +++ b/.benchmark/asset/frontend/app/ๆŠ˜ๆ—ง่ฎก็ฎ—/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๆŠ˜ๆ—ง่ฎก็ฎ— } from "@/hooks/useๆŠ˜ๆ—ง่ฎก็ฎ—"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๆŠ˜ๆ—ง่ฎก็ฎ—(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆŠ˜ๆ—ง่ฎก็ฎ—

+

ๆŠ˜ๆ—ง่ฎก็ฎ—

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/็›˜็‚น็ปŸ่ฎก/page.tsx b/.benchmark/asset/frontend/app/็›˜็‚น็ปŸ่ฎก/page.tsx new file mode 100644 index 0000000..7298592 --- /dev/null +++ b/.benchmark/asset/frontend/app/็›˜็‚น็ปŸ่ฎก/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use็›˜็‚น็ปŸ่ฎก } from "@/hooks/use็›˜็‚น็ปŸ่ฎก"; + +export default function PagePage() { + const { data, loading, error, refetch } = use็›˜็‚น็ปŸ่ฎก(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

็›˜็‚น็ปŸ่ฎก

+

็›˜็‚น็ปŸ่ฎก

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/่ต„ไบง็™ป่ฎฐ/page.tsx b/.benchmark/asset/frontend/app/่ต„ไบง็™ป่ฎฐ/page.tsx new file mode 100644 index 0000000..05fae34 --- /dev/null +++ b/.benchmark/asset/frontend/app/่ต„ไบง็™ป่ฎฐ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use่ต„ไบง็™ป่ฎฐ } from "@/hooks/use่ต„ไบง็™ป่ฎฐ"; + +export default function PagePage() { + const { data, loading, error, refetch } = use่ต„ไบง็™ป่ฎฐ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

่ต„ไบง็™ป่ฎฐ

+

่ต„ไบง็™ป่ฎฐ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/app/้ข†็”จๅฝ’่ฟ˜/page.tsx b/.benchmark/asset/frontend/app/้ข†็”จๅฝ’่ฟ˜/page.tsx new file mode 100644 index 0000000..c2190c9 --- /dev/null +++ b/.benchmark/asset/frontend/app/้ข†็”จๅฝ’่ฟ˜/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use้ข†็”จๅฝ’่ฟ˜ } from "@/hooks/use้ข†็”จๅฝ’่ฟ˜"; + +export default function PagePage() { + const { data, loading, error, refetch } = use้ข†็”จๅฝ’่ฟ˜(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

้ข†็”จๅฝ’่ฟ˜

+

้ข†็”จๅฝ’่ฟ˜

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/layout/BottomNav.tsx b/.benchmark/asset/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/asset/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/asset/frontend/components/layout/Sidebar.tsx b/.benchmark/asset/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/asset/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/asset/frontend/components/ui/Button.tsx b/.benchmark/asset/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/asset/frontend/components/ui/Card.tsx b/.benchmark/asset/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/ui/EmptyState.tsx b/.benchmark/asset/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/ui/Input.tsx b/.benchmark/asset/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/asset/frontend/components/ui/Modal.tsx b/.benchmark/asset/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/asset/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/asset/frontend/hooks/useAssets.ts b/.benchmark/asset/frontend/hooks/useAssets.ts new file mode 100644 index 0000000..2645554 --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useAssets.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for assets +type Asset = Record; + +/** + * Fetch and manage assets data. + */ +export function useAssets() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/assets`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useAuth.ts b/.benchmark/asset/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useDebounce.ts b/.benchmark/asset/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/asset/frontend/hooks/useForm.ts b/.benchmark/asset/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/asset/frontend/hooks/useHealth.ts b/.benchmark/asset/frontend/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useItem.ts b/.benchmark/asset/frontend/hooks/useItem.ts new file mode 100644 index 0000000..65342ec --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for ้ข†็”จๅฝ’่ฟ˜ +type Item = Record; + +/** + * Fetch and manage ้ข†็”จๅฝ’่ฟ˜ data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/้ข†็”จๅฝ’่ฟ˜`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useItems.ts b/.benchmark/asset/frontend/hooks/useItems.ts new file mode 100644 index 0000000..ef60559 --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useItems.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for items +type Item = Record; + +/** + * Fetch and manage items data. + */ +export function useItems() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/items`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useStats.ts b/.benchmark/asset/frontend/hooks/useStats.ts new file mode 100644 index 0000000..f9f34cf --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useStats.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for stats +type Stat = Record; + +/** + * Fetch and manage stats data. + */ +export function useStats() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/stats`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/hooks/useUsers.ts b/.benchmark/asset/frontend/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/asset/frontend/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/frontend/next-env.d.ts b/.benchmark/asset/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/asset/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/asset/frontend/next.config.ts b/.benchmark/asset/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/asset/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/asset/frontend/package.json b/.benchmark/asset/frontend/package.json new file mode 100644 index 0000000..caa9eab --- /dev/null +++ b/.benchmark/asset/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/frontend/postcss.config.js b/.benchmark/asset/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/asset/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/asset/frontend/services/api.ts b/.benchmark/asset/frontend/services/api.ts new file mode 100644 index 0000000..13abf7f --- /dev/null +++ b/.benchmark/asset/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for MyProject + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/asset/frontend/services/assets.ts b/.benchmark/asset/frontend/services/assets.ts new file mode 100644 index 0000000..976b03b --- /dev/null +++ b/.benchmark/asset/frontend/services/assets.ts @@ -0,0 +1,10 @@ +// Auto-generated assets service +import { api } from "./api"; + +export function get() { + return api.get("/api/assets"); +} + +export function post(data: Record) { + return api.post("/api/assets", data); +} diff --git a/.benchmark/asset/frontend/services/auth.ts b/.benchmark/asset/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/asset/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/asset/frontend/services/health.ts b/.benchmark/asset/frontend/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/asset/frontend/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/asset/frontend/services/items.ts b/.benchmark/asset/frontend/services/items.ts new file mode 100644 index 0000000..f9825fa --- /dev/null +++ b/.benchmark/asset/frontend/services/items.ts @@ -0,0 +1,18 @@ +// Auto-generated items service +import { api } from "./api"; + +export function get() { + return api.get("/api/items"); +} + +export function post(data: Record) { + return api.post("/api/items", data); +} + +export function get() { + return api.get("/api/items"); +} + +export function post(data: Record) { + return api.post("/api/items", data); +} diff --git a/.benchmark/asset/frontend/services/stats.ts b/.benchmark/asset/frontend/services/stats.ts new file mode 100644 index 0000000..e48d921 --- /dev/null +++ b/.benchmark/asset/frontend/services/stats.ts @@ -0,0 +1,10 @@ +// Auto-generated stats service +import { api } from "./api"; + +export function get() { + return api.get("/api/stats"); +} + +export function post(data: Record) { + return api.post("/api/stats", data); +} diff --git a/.benchmark/asset/frontend/services/users.ts b/.benchmark/asset/frontend/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/asset/frontend/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/asset/frontend/services/ๆŠ˜ๆ—ง่ฎก็ฎ—.ts b/.benchmark/asset/frontend/services/ๆŠ˜ๆ—ง่ฎก็ฎ—.ts new file mode 100644 index 0000000..0db59b0 --- /dev/null +++ b/.benchmark/asset/frontend/services/ๆŠ˜ๆ—ง่ฎก็ฎ—.ts @@ -0,0 +1,10 @@ +// Auto-generated ๆŠ˜ๆ—ง่ฎก็ฎ— service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๆŠ˜ๆ—ง่ฎก็ฎ—"); +} + +export function post(data: Record) { + return api.post("/api/ๆŠ˜ๆ—ง่ฎก็ฎ—", data); +} diff --git a/.benchmark/asset/frontend/services/็›˜็‚น็ปŸ่ฎก.ts b/.benchmark/asset/frontend/services/็›˜็‚น็ปŸ่ฎก.ts new file mode 100644 index 0000000..b677698 --- /dev/null +++ b/.benchmark/asset/frontend/services/็›˜็‚น็ปŸ่ฎก.ts @@ -0,0 +1,10 @@ +// Auto-generated ็›˜็‚น็ปŸ่ฎก service +import { api } from "./api"; + +export function get() { + return api.get("/api/็›˜็‚น็ปŸ่ฎก"); +} + +export function post(data: Record) { + return api.post("/api/็›˜็‚น็ปŸ่ฎก", data); +} diff --git a/.benchmark/asset/frontend/services/่ต„ไบง็™ป่ฎฐ.ts b/.benchmark/asset/frontend/services/่ต„ไบง็™ป่ฎฐ.ts new file mode 100644 index 0000000..9b13293 --- /dev/null +++ b/.benchmark/asset/frontend/services/่ต„ไบง็™ป่ฎฐ.ts @@ -0,0 +1,10 @@ +// Auto-generated ่ต„ไบง็™ป่ฎฐ service +import { api } from "./api"; + +export function get() { + return api.get("/api/่ต„ไบง็™ป่ฎฐ"); +} + +export function post(data: Record) { + return api.post("/api/่ต„ไบง็™ป่ฎฐ", data); +} diff --git a/.benchmark/asset/frontend/services/้ข†็”จๅฝ’่ฟ˜.ts b/.benchmark/asset/frontend/services/้ข†็”จๅฝ’่ฟ˜.ts new file mode 100644 index 0000000..c2ec9cc --- /dev/null +++ b/.benchmark/asset/frontend/services/้ข†็”จๅฝ’่ฟ˜.ts @@ -0,0 +1,10 @@ +// Auto-generated ้ข†็”จๅฝ’่ฟ˜ service +import { api } from "./api"; + +export function get() { + return api.get("/api/้ข†็”จๅฝ’่ฟ˜"); +} + +export function post(data: Record) { + return api.post("/api/้ข†็”จๅฝ’่ฟ˜", data); +} diff --git a/.benchmark/asset/frontend/tailwind.config.ts b/.benchmark/asset/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/asset/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/asset/frontend/tsconfig.json b/.benchmark/asset/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/asset/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/frontend/types/index.ts b/.benchmark/asset/frontend/types/index.ts new file mode 100644 index 0000000..e1b2628 --- /dev/null +++ b/.benchmark/asset/frontend/types/index.ts @@ -0,0 +1,120 @@ +// Auto-generated types for MyProject + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/.gitignore b/.benchmark/asset/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/asset/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/asset/fullstack/README.md b/.benchmark/asset/fullstack/README.md new file mode 100644 index 0000000..e33895f --- /dev/null +++ b/.benchmark/asset/fullstack/README.md @@ -0,0 +1,118 @@ +# MyProject โ€” Fullstack Project + +> ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚ + +## Architecture + +``` +apps/ +โ”œโ”€โ”€ web/ # Next.js 15 + TypeScript + Tailwind CSS +โ””โ”€โ”€ api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +โ”œโ”€โ”€ shared-types/ # @shared/types โ€” shared TypeScript interfaces +โ””โ”€โ”€ shared-config/ # @shared/config โ€” shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` โ€” List +- `GET /api/users/:id` โ€” Get +- `POST /api/users` โ€” Create +- `PUT /api/users/:id` โ€” Update +- `DELETE /api/users/:id` โ€” Delete + +#### contracts +- `GET /api/contracts` โ€” List +- `GET /api/contracts/:id` โ€” Get +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List +- `GET /api/approvals/:id` โ€” Get +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List +- `GET /api/customers/:id` โ€” Get +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List +- `GET /api/reminders/:id` โ€” Get +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### assets +- `GET /api/assets` โ€” List +- `GET /api/assets/:id` โ€” Get +- `POST /api/assets` โ€” Create +- `PUT /api/assets/:id` โ€” Update +- `DELETE /api/assets/:id` โ€” Delete + +#### items +- `GET /api/items` โ€” List +- `GET /api/items/:id` โ€” Get +- `POST /api/items` โ€” Create +- `PUT /api/items/:id` โ€” Update +- `DELETE /api/items/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List +- `GET /api/stats/:id` โ€” Get +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/asset/fullstack/apps/api/.gitignore b/.benchmark/asset/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/asset/fullstack/apps/api/README.md b/.benchmark/asset/fullstack/apps/api/README.md new file mode 100644 index 0000000..8983604 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/README.md @@ -0,0 +1,111 @@ +# MyProject โ€” Backend API + +> ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### assets +- `GET /api/assets` โ€” List all +- `GET /api/assets/:id` โ€” Get by ID +- `POST /api/assets` โ€” Create +- `PUT /api/assets/:id` โ€” Update +- `DELETE /api/assets/:id` โ€” Delete + +#### items +- `GET /api/items` โ€” List all +- `GET /api/items/:id` โ€” Get by ID +- `POST /api/items` โ€” Create +- `PUT /api/items/:id` โ€” Update +- `DELETE /api/items/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List all +- `GET /api/stats/:id` โ€” Get by ID +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/asset/fullstack/apps/api/package.json b/.benchmark/asset/fullstack/apps/api/package.json new file mode 100644 index 0000000..2831924 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "myproject-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts b/.benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts new file mode 100644 index 0000000..ff42fbb --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/__tests__/items.test.ts @@ -0,0 +1,119 @@ +// Items CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/asset/fullstack/apps/api/src/db/client.ts b/.benchmark/asset/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/asset/fullstack/apps/api/src/db/schema.ts b/.benchmark/asset/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..425ac3a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,19 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assets (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n category TEXT,\n purchase_date TEXT,\n purchase_price REAL,\n current_value REAL,\n location TEXT,\n status TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS items (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n metric TEXT NOT NULL,\n value REAL,\n period TEXT,\n recorded_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/index.ts b/.benchmark/asset/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..345d1ec --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/index.ts @@ -0,0 +1,77 @@ +// MyProject โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { assetsRoutes } from "./routes/assets.js"; +import { itemsRoutes } from "./routes/items.js"; +import { statsRoutes } from "./routes/stats.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-8dd37ee5"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(assetsRoutes, { prefix: "/api/assets" }); + await app.register(itemsRoutes, { prefix: "/api/items" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/asset/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/routes/auth.ts b/.benchmark/asset/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/routes/items.ts b/.benchmark/asset/fullstack/apps/api/src/routes/items.ts new file mode 100644 index 0000000..a649fff --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Items routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemsService } from "../services/item.js"; +import type { CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +const service = new ItemsService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items โ€” create + app.post<{ Body: CreateItemsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateItemsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/routes/users.ts b/.benchmark/asset/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/asset/fullstack/apps/api/src/services/item.ts b/.benchmark/asset/fullstack/apps/api/src/services/item.ts new file mode 100644 index 0000000..6ec5f12 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/services/item.ts @@ -0,0 +1,54 @@ +// Auto-generated Items service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Items, CreateItemsInput, UpdateItemsInput } from "../types/index.js"; + +export class ItemsService { + /** List all items */ + list(): Items[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Items | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemsInput): Items { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemsInput): Items | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/services/user.ts b/.benchmark/asset/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/asset/fullstack/apps/api/src/types/index.ts b/.benchmark/asset/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..74d1918 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,203 @@ +// Import and re-export shared entity types +import type { + User, + Contract, + Approval, + Customer, + Reminder, + Assets, + Items, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Contract, + Approval, + Customer, + Reminder, + Assets, + Items, + Stats, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateAssetsInput { + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface CreateItemsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface CreateStatsInput { + userId?: string; + metric: string; + value?: number; + period?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateAssetsInput { + userId?: string; + name?: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; +} + +export interface UpdateItemsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +export interface UpdateStatsInput { + userId?: string; + metric?: string; + value?: number; + period?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/asset/fullstack/apps/api/tsconfig.json b/.benchmark/asset/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/asset/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/web/app/feature/page.tsx b/.benchmark/asset/fullstack/apps/web/app/feature/page.tsx new file mode 100644 index 0000000..729c2d9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/feature/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅŠŸ่ƒฝ้กต

+

ๆ ธๅฟƒๅŠŸ่ƒฝ้กต้ข

+
+ +
+ + { +

ๅŠŸ่ƒฝ้กต ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /feature

+
} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/app/globals.css b/.benchmark/asset/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/asset/fullstack/apps/web/app/layout.tsx b/.benchmark/asset/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..43f1cb9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "MyProject", + description: "ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/app/page.tsx b/.benchmark/asset/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..203f24f --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

MyProject

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ่ต„ไบง็™ป่ฎฐ + + + ๐Ÿ“… + ้ข†็”จๅฝ’่ฟ˜ + + + ๐Ÿ“ + ๆŠ˜ๆ—ง่ฎก็ฎ— + + + ๐Ÿ“ธ + ็›˜็‚น็ปŸ่ฎก + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ MyProject

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/app/profile/page.tsx b/.benchmark/asset/fullstack/apps/web/app/profile/page.tsx new file mode 100644 index 0000000..61ddaaa --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆˆ‘็š„

+

็”จๆˆทไธญๅฟƒ

+
+ +
+ + { +

ๆˆ‘็š„ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile

+
} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useForm.ts b/.benchmark/asset/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useHealth.ts b/.benchmark/asset/fullstack/apps/web/hooks/useHealth.ts new file mode 100644 index 0000000..9aa622a --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useHealth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for health +type Health = Record; + +/** + * Fetch and manage health data. + */ +export function useHealth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/health`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/fullstack/apps/web/hooks/useUsers.ts b/.benchmark/asset/fullstack/apps/web/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/asset/fullstack/apps/web/next.config.ts b/.benchmark/asset/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/asset/fullstack/apps/web/package.json b/.benchmark/asset/fullstack/apps/web/package.json new file mode 100644 index 0000000..0e5a224 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "myproject-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/web/postcss.config.js b/.benchmark/asset/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/asset/fullstack/apps/web/services/api.ts b/.benchmark/asset/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..b2e1f85 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for MyProject + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/asset/fullstack/apps/web/services/health.ts b/.benchmark/asset/fullstack/apps/web/services/health.ts new file mode 100644 index 0000000..4fdf89e --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/services/health.ts @@ -0,0 +1,6 @@ +// Auto-generated health service +import { api } from "./api"; + +export function get() { + return api.get("/api/health"); +} diff --git a/.benchmark/asset/fullstack/apps/web/services/users.ts b/.benchmark/asset/fullstack/apps/web/services/users.ts new file mode 100644 index 0000000..03fad78 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/services/users.ts @@ -0,0 +1,10 @@ +// Auto-generated users service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/users", data); +} + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/asset/fullstack/apps/web/tailwind.config.ts b/.benchmark/asset/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/asset/fullstack/apps/web/tsconfig.json b/.benchmark/asset/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/apps/web/types/index.ts b/.benchmark/asset/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..e1b2628 --- /dev/null +++ b/.benchmark/asset/fullstack/apps/web/types/index.ts @@ -0,0 +1,120 @@ +// Auto-generated types for MyProject + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/package.json b/.benchmark/asset/fullstack/package.json new file mode 100644 index 0000000..a32255b --- /dev/null +++ b/.benchmark/asset/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "myproject", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-config/package.json b/.benchmark/asset/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-config/src/index.ts b/.benchmark/asset/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL โ€” reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/asset/fullstack/packages/shared-config/tsconfig.json b/.benchmark/asset/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-types/package.json b/.benchmark/asset/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/packages/shared-types/src/index.ts b/.benchmark/asset/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..cc57d56 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,122 @@ +// Shared types for fullstack project +// Auto-generated โ€” used by both apps/web and apps/api + +// โ”€โ”€โ”€ Entities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Assets { + id: string; + userId?: string; + name: string; + category?: string; + purchaseDate?: string; + purchasePrice?: number; + currentValue?: number; + location?: string; + status?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Items { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +// โ”€โ”€โ”€ API Response Wrappers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/asset/fullstack/packages/shared-types/tsconfig.json b/.benchmark/asset/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/asset/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/fullstack/scripts/build.mjs b/.benchmark/asset/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/asset/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script โ€” builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n๐Ÿ”จ ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("๐Ÿ—๏ธ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\nโœ… Build complete!"); +} catch (e) { + console.error("\nโŒ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/asset/fullstack/scripts/dev.mjs b/.benchmark/asset/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/asset/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script โ€” starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("๐Ÿš€ Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/asset/prd.json b/.benchmark/asset/prd.json new file mode 100644 index 0000000..a8768b5 --- /dev/null +++ b/.benchmark/asset/prd.json @@ -0,0 +1 @@ +{"projectName":"MyProject","chineseName":"้€š็”จๅบ”็”จ","domain":"generic","matchConfidence":"low","summary":"ไธ€ๆฌพๆ นๆฎ็”จๆˆท้œ€ๆฑ‚ๅฎšๅˆถ็š„ๅบ”็”จใ€‚","personas":[{"name":"็”จๆˆท","description":"็›ฎๆ ‡็”จๆˆท็พคไฝ“","painPoints":["ๅพ…ๆ˜Ž็กฎ"]}],"userStories":[{"id":"US-001","as":"็”จๆˆท","want":"่ต„ไบง็™ป่ฎฐ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅพ…ๆ˜Ž็กฎ","priority":"P0"},{"id":"US-002","as":"็”จๆˆท","want":"้ข†็”จๅฝ’่ฟ˜","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅพ…ๆ˜Ž็กฎ","priority":"P0"},{"id":"US-003","as":"็”จๆˆท","want":"ๆŠ˜ๆ—ง่ฎก็ฎ—","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅพ…ๆ˜Ž็กฎ","priority":"P0"}],"mvpScope":{"description":"MVP ่š็„ฆ ่ต„ไบง็™ป่ฎฐใ€้ข†็”จๅฝ’่ฟ˜ใ€ๆŠ˜ๆ—ง่ฎก็ฎ—ใ€็›˜็‚น็ปŸ่ฎก๏ผŒๅŒ…ๅซ้กต้ข๏ผš้ฆ–้กตใ€่ต„ไบง็™ป่ฎฐใ€้ข†็”จๅฝ’่ฟ˜","features":["่ต„ไบง็™ป่ฎฐ","้ข†็”จๅฝ’่ฟ˜","ๆŠ˜ๆ—ง่ฎก็ฎ—","็›˜็‚น็ปŸ่ฎก"],"pages":["้ฆ–้กต","่ต„ไบง็™ป่ฎฐ","้ข†็”จๅฝ’่ฟ˜"],"estimatedWeeks":3},"features":[{"name":"่ต„ไบง็™ป่ฎฐ","description":"่ต„ไบง็™ป่ฎฐ","priority":"P0"},{"name":"้ข†็”จๅฝ’่ฟ˜","description":"้ข†็”จๅฝ’่ฟ˜","priority":"P0"},{"name":"ๆŠ˜ๆ—ง่ฎก็ฎ—","description":"ๆŠ˜ๆ—ง่ฎก็ฎ—","priority":"P0"},{"name":"็›˜็‚น็ปŸ่ฎก","description":"็›˜็‚น็ปŸ่ฎก","priority":"P0"}],"pages":[{"name":"้ฆ–้กต","route":"/home","description":"ๅบ”็”จ้ฆ–้กต"},{"name":"่ต„ไบง็™ป่ฎฐ","route":"/assets","description":"่ต„ไบง็™ป่ฎฐ"},{"name":"้ข†็”จๅฝ’่ฟ˜","route":"/items","description":"้ข†็”จๅฝ’่ฟ˜"},{"name":"ๆŠ˜ๆ—ง่ฎก็ฎ—","route":"/items","description":"ๆŠ˜ๆ—ง่ฎก็ฎ—"},{"name":"็›˜็‚น็ปŸ่ฎก","route":"/stats","description":"็›˜็‚น็ปŸ่ฎก"}],"apiRequirements":[{"method":"GET","path":"/api/assets","description":"่Žทๅ–่ต„ไบง็™ป่ฎฐๅˆ—่กจ"},{"method":"POST","path":"/api/assets","description":"ๅˆ›ๅปบ่ต„ไบง็™ป่ฎฐ"},{"method":"GET","path":"/api/items","description":"่Žทๅ–้ข†็”จๅฝ’่ฟ˜ๅˆ—่กจ"},{"method":"POST","path":"/api/items","description":"ๅˆ›ๅปบ้ข†็”จๅฝ’่ฟ˜"},{"method":"GET","path":"/api/items","description":"่Žทๅ–ๆŠ˜ๆ—ง่ฎก็ฎ—ๅˆ—่กจ"},{"method":"POST","path":"/api/items","description":"ๅˆ›ๅปบๆŠ˜ๆ—ง่ฎก็ฎ—"},{"method":"GET","path":"/api/stats","description":"่Žทๅ–็›˜็‚น็ปŸ่ฎกๅˆ—่กจ"},{"method":"POST","path":"/api/stats","description":"ๅˆ›ๅปบ็›˜็‚น็ปŸ่ฎก"},{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter๏ผˆ่ทจๅนณๅฐ๏ผ‰","considerations":["ๅปบ่ฎฎ่ทจๅนณๅฐๆก†ๆžถๅ‡ๅฐ‘ๅผ€ๅ‘ๆˆๆœฌ"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"้กน็›ฎ่„šๆ‰‹ๆžถๆญๅปบ","description":"ๅˆๅง‹ๅŒ– MyProject ้กน็›ฎ็ป“ๆž„๏ผŒ้…็ฝฎๆž„ๅปบๅทฅๅ…ทๅ’Œ CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"ๆ•ฐๆฎๅบ“่ฎพ่ฎกไธŽๅˆๅง‹ๅŒ–","description":"่ฎพ่ฎกๆ•ฐๆฎๆจกๅž‹๏ผŒๅˆ›ๅปบๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌ","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"้กต้ขๅผ€ๅ‘๏ผš้ฆ–้กต","description":"ๅผ€ๅ‘ ้ฆ–้กต ้กต้ข๏ผˆ/home๏ผ‰๏ผšๅบ”็”จ้ฆ–้กต","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"้กต้ขๅผ€ๅ‘๏ผš่ต„ไบง็™ป่ฎฐ","description":"ๅผ€ๅ‘ ่ต„ไบง็™ป่ฎฐ ้กต้ข๏ผˆ/assets๏ผ‰๏ผš่ต„ไบง็™ป่ฎฐ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"้กต้ขๅผ€ๅ‘๏ผš้ข†็”จๅฝ’่ฟ˜","description":"ๅผ€ๅ‘ ้ข†็”จๅฝ’่ฟ˜ ้กต้ข๏ผˆ/items๏ผ‰๏ผš้ข†็”จๅฝ’่ฟ˜","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"้กต้ขๅผ€ๅ‘๏ผšๆŠ˜ๆ—ง่ฎก็ฎ—","description":"ๅผ€ๅ‘ ๆŠ˜ๆ—ง่ฎก็ฎ— ้กต้ข๏ผˆ/items๏ผ‰๏ผšๆŠ˜ๆ—ง่ฎก็ฎ—","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API ๅผ€ๅ‘๏ผšGET /api/assets","description":"่Žทๅ–่ต„ไบง็™ป่ฎฐๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API ๅผ€ๅ‘๏ผšPOST /api/assets","description":"ๅˆ›ๅปบ่ต„ไบง็™ป่ฎฐ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API ๅผ€ๅ‘๏ผšGET /api/items","description":"่Žทๅ–้ข†็”จๅฝ’่ฟ˜ๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API ๅผ€ๅ‘๏ผšPOST /api/items","description":"ๅˆ›ๅปบ้ข†็”จๅฝ’่ฟ˜","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"ๅ‰ๅŽ็ซฏ่”่ฐƒ","description":"ๅ‰็ซฏ้กต้ขไธŽๅŽ็ซฏ API ้›†ๆˆ่”่ฐƒ","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"ๆต‹่ฏ•ไธŽไฟฎๅค","description":"ๅŠŸ่ƒฝๆต‹่ฏ•ใ€Bug ไฟฎๅคใ€ๆ€ง่ƒฝไผ˜ๅŒ–","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:36.980Z","inputLength":33,"domainMatchCount":0,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/asset/release/release/README.md b/.benchmark/asset/release/release/README.md new file mode 100644 index 0000000..fae49a5 --- /dev/null +++ b/.benchmark/asset/release/release/README.md @@ -0,0 +1,33 @@ +# myproject โ€” Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:37.343Z + +## Directory Structure + +``` +release/ +โ”œโ”€โ”€ version.json โ€” Version metadata +โ”œโ”€โ”€ build-info.json โ€” Build environment info +โ”œโ”€โ”€ README.md โ€” This file +โ”œโ”€โ”€ manifests/ +โ”‚ โ””โ”€โ”€ manifest.json โ€” Release manifest with file hashes +โ”œโ”€โ”€ checksums/ +โ”‚ โ””โ”€โ”€ checksums.txt โ€” SHA256 checksums for verification +โ”œโ”€โ”€ release-notes/ +โ”‚ โ””โ”€โ”€ release-notes.md โ€” Human-readable release notes +โ”œโ”€โ”€ windows/ โ€” Windows installers +โ”œโ”€โ”€ macos/ โ€” macOS disk images +โ””โ”€โ”€ linux/ โ€” Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/asset/release/release/build-info.json b/.benchmark/asset/release/release/build-info.json new file mode 100644 index 0000000..bde0e23 --- /dev/null +++ b/.benchmark/asset/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:37.343Z", + "domain": "unknown", + "entityCount": 9, + "routeCount": 1, + "screenCount": 9, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "myproject", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/asset/release/release/checksums/checksums.txt b/.benchmark/asset/release/release/checksums/checksums.txt new file mode 100644 index 0000000..ce01b57 --- /dev/null +++ b/.benchmark/asset/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +be58304f2685656f24eea61b6f669909fcb4bba6033498c00ca929ddb9494b8e windows/myproject-setup-0.1.0.exe +cf95a39a77b8a45d00f09928cd9d14e7e0c9529065813470fac7e49b8db78925 windows/myproject-0.1.0-portable.exe +f072a9bc11e8123b5b4df0725cd0ffe8b550b0ab56f5b5441a1eec6ed34489eb macos/myproject-0.1.0.dmg +293084b0e5e79005ac0f864d0c773abfc06b143aff2bbfa05b36d66a8aceb2c2 macos/myproject-0.1.0-arm64.dmg +6796cb2b246f940d02372542e3005cbb70df962ca7377a753691f37572fff99b linux/myproject-0.1.0.AppImage +1ff107aa051cbe6724f19fba5e5411850f392ccd23af95fad7514bb490afd91f linux/myproject_0.1.0_amd64.deb diff --git a/.benchmark/asset/release/release/linux/myproject-0.1.0.AppImage b/.benchmark/asset/release/release/linux/myproject-0.1.0.AppImage new file mode 100644 index 0000000..91107d8 --- /dev/null +++ b/.benchmark/asset/release/release/linux/myproject-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb b/.benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb new file mode 100644 index 0000000..128633b --- /dev/null +++ b/.benchmark/asset/release/release/linux/myproject_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg b/.benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg new file mode 100644 index 0000000..5227ab3 --- /dev/null +++ b/.benchmark/asset/release/release/macos/myproject-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/macos/myproject-0.1.0.dmg b/.benchmark/asset/release/release/macos/myproject-0.1.0.dmg new file mode 100644 index 0000000..4ad7e6c --- /dev/null +++ b/.benchmark/asset/release/release/macos/myproject-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/manifests/manifest.json b/.benchmark/asset/release/release/manifests/manifest.json new file mode 100644 index 0000000..9d88664 --- /dev/null +++ b/.benchmark/asset/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:37.343Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/myproject-setup-0.1.0.exe", + "size": 144, + "sha256": "be58304f2685656f24eea61b6f669909fcb4bba6033498c00ca929ddb9494b8e" + }, + { + "path": "windows/myproject-0.1.0-portable.exe", + "size": 148, + "sha256": "cf95a39a77b8a45d00f09928cd9d14e7e0c9529065813470fac7e49b8db78925" + }, + { + "path": "macos/myproject-0.1.0.dmg", + "size": 140, + "sha256": "f072a9bc11e8123b5b4df0725cd0ffe8b550b0ab56f5b5441a1eec6ed34489eb" + }, + { + "path": "macos/myproject-0.1.0-arm64.dmg", + "size": 142, + "sha256": "293084b0e5e79005ac0f864d0c773abfc06b143aff2bbfa05b36d66a8aceb2c2" + }, + { + "path": "linux/myproject-0.1.0.AppImage", + "size": 133, + "sha256": "6796cb2b246f940d02372542e3005cbb70df962ca7377a753691f37572fff99b" + }, + { + "path": "linux/myproject_0.1.0_amd64.deb", + "size": 143, + "sha256": "1ff107aa051cbe6724f19fba5e5411850f392ccd23af95fad7514bb490afd91f" + } + ], + "totalFiles": 6, + "totalSize": 850 +} \ No newline at end of file diff --git a/.benchmark/asset/release/release/release-notes/release-notes.md b/.benchmark/asset/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..4e40d22 --- /dev/null +++ b/.benchmark/asset/release/release/release-notes/release-notes.md @@ -0,0 +1,58 @@ +# myproject v0.1.0 + +> Release Builder Agent โ€” SF-07 +> Generated: 2026-06-05 22:08:37 UTC + +## Version + +- **Name:** myproject +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- approvals +- assets +- auth +- contracts +- customers +- items +- reminders +- stats +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:37.343Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- โœ… windows +- โœ… macos +- โœ… linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/asset/release/release/version.json b/.benchmark/asset/release/release/version.json new file mode 100644 index 0000000..3a7e2b5 --- /dev/null +++ b/.benchmark/asset/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "myproject", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe b/.benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe new file mode 100644 index 0000000..fa4b1a1 --- /dev/null +++ b/.benchmark/asset/release/release/windows/myproject-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe b/.benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe new file mode 100644 index 0000000..b7adc7d --- /dev/null +++ b/.benchmark/asset/release/release/windows/myproject-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] myproject v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:37.342Z diff --git a/.benchmark/blog-cms/arch.json b/.benchmark/blog-cms/arch.json new file mode 100644 index 0000000..b314723 --- /dev/null +++ b/.benchmark/blog-cms/arch.json @@ -0,0 +1 @@ +{"projectName":"SocialApp","domain":"social","contract":{"projectName":"SocialApp","domain":"social","entities":[{"name":"User","table":"users","description":"็ณป็ปŸ็”จๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"ๅˆๅŒ","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"ๅฎกๆ‰นๆต็จ‹","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"ๅฎขๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"ๅˆฐๆœŸๆ้†’","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Articles","table":"articles","description":"ๆ–‡็ซ ๅ‘ๅธƒ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"content","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"excerpt","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"cover_url","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"author_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"authors","fkColumn":"id"},{"name":"category","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"published_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"authors","via":"author_id"}]},{"name":"Tags","table":"tags","description":"ๅˆ†็ฑปๆ ‡็ญพ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false,"unique":true},{"name":"color","type":"VARCHAR(7)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Comments","table":"comments","description":"่ฏ„่ฎบ็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true,"isPrimary":false,"isAuto":false},{"name":"entity_id","type":"UUID","required":true,"isPrimary":false,"isAuto":false},{"name":"content","type":"TEXT","required":true,"isPrimary":false,"isAuto":false},{"name":"parent_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"parents","fkColumn":"id"},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"parents","via":"parent_id"}]},{"name":"Media","table":"media","description":"ๅช’ไฝ“ๅบ“่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"filename","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"url","type":"TEXT","required":true,"isPrimary":false,"isAuto":false},{"name":"mime_type","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"size_bytes","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","cover_url":"coverUrl","author_id":"authorId","published_at":"publishedAt","parent_id":"parentId","mime_type":"mimeType","size_bytes":"sizeBytes","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","coverUrl":"cover_url","authorId":"author_id","publishedAt":"published_at","parentId":"parent_id","mimeType":"mime_type","sizeBytes":"size_bytes","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Articles":{"id":{"required":true},"title":{"required":true}},"Tags":{"id":{"required":true},"name":{"required":true,"unique":true}},"Comments":{"id":{"required":true},"entity_type":{"required":true},"entity_id":{"required":true},"content":{"required":true}},"Media":{"id":{"required":true},"filename":{"required":true},"url":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO๏ผˆๆ–‡ไปถๅญ˜ๅ‚จ๏ผ‰","deployment":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","considerations":[]},"architectureDiagram":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ SocialApp โ€” System Architecture โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Client Layer โ”‚ โ”‚ Admin / Web โ”‚ โ”‚\nโ”‚ โ”‚ React Native 0.76 + Expo / Flutter 3.x โ”‚ โ”‚ React + Vite (Web) โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ API Gateway โ”‚ โ”‚\nโ”‚ โ”‚ Nginx / Kong โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Backend Services โ”‚ โ”‚\nโ”‚ โ”‚ NestJS + Prisma โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ PostgreSQL 15โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ MinIO โ”‚ โ”‚\nโ”‚ โ”‚ Primary โ”‚ โ”‚ Session โ”‚ โ”‚ Files โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ Modules: โ”‚\nโ”‚ 1 ๆ–‡็ซ ๅ‘ๅธƒ โ”‚\nโ”‚ 2 ๅˆ†็ฑปๆ ‡็ญพ โ”‚\nโ”‚ 3 Content โ”‚\nโ”‚ 4 ๅช’ไฝ“ๅบ“ โ”‚\nโ”‚ โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","dataFlows":[{"page":"ๆ–‡็ซ ๅ‘ๅธƒ","route":"/articles","description":"ๆ–‡็ซ ๅ‘ๅธƒ","dataFlow":["GET /api/articles โ†’ ่Žทๅ–ๆ–‡็ซ ๅ‘ๅธƒๅˆ—่กจ","POST /api/articles โ†’ ๅˆ›ๅปบๆ–‡็ซ ๅ‘ๅธƒ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅˆ†็ฑปๆ ‡็ญพ","route":"/tags","description":"ๅˆ†็ฑปๆ ‡็ญพ","dataFlow":["GET /api/tags โ†’ ่Žทๅ–ๅˆ†็ฑปๆ ‡็ญพๅˆ—่กจ","POST /api/tags โ†’ ๅˆ›ๅปบๅˆ†็ฑปๆ ‡็ญพ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"่ฏ„่ฎบ็ฎก็†","route":"/comments","description":"่ฏ„่ฎบ็ฎก็†","dataFlow":["GET /api/comments โ†’ ่Žทๅ–่ฏ„่ฎบ็ฎก็†ๅˆ—่กจ","POST /api/comments โ†’ ๅˆ›ๅปบ่ฏ„่ฎบ็ฎก็†"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅช’ไฝ“ๅบ“","route":"/media","description":"ๅช’ไฝ“ๅบ“","dataFlow":["GET /api/media โ†’ ่Žทๅ–ๅช’ไฝ“ๅบ“ๅˆ—่กจ","POST /api/media โ†’ ๅˆ›ๅปบๅช’ไฝ“ๅบ“"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"}],"modules":[{"name":"ๆ–‡็ซ ๅ‘ๅธƒ","label":"ๆ–‡็ซ ๅ‘ๅธƒ","features":["ๆ–‡็ซ ๅ‘ๅธƒ"],"responsibilities":["ๆไพ› ๆ–‡็ซ ๅ‘ๅธƒ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ๅˆ†็ฑปๆ ‡็ญพ","label":"ๅˆ†็ฑปๆ ‡็ญพ","features":["ๅˆ†็ฑปๆ ‡็ญพ"],"responsibilities":["ๆไพ› ๅˆ†็ฑปๆ ‡็ญพ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"content","label":"Content","features":["่ฏ„่ฎบ็ฎก็†"],"responsibilities":["ๆไพ› ่ฏ„่ฎบ็ฎก็† ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ๅช’ไฝ“ๅบ“","label":"ๅช’ไฝ“ๅบ“","features":["ๅช’ไฝ“ๅบ“"],"responsibilities":["ๆไพ› ๅช’ไฝ“ๅบ“ ็›ธๅ…ณๅŠŸ่ƒฝ"]}],"databaseSchema":[{"table":"users","description":"็”จๆˆท่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"articles","description":"ๆ–‡็ซ ๅ‘ๅธƒ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"content","type":"TEXT"},{"name":"excerpt","type":"TEXT"},{"name":"cover_url","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"author_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"category","type":"VARCHAR(64)"},{"name":"published_at","type":"TIMESTAMPTZ"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_articles_user ON articles(user_id)"]},{"table":"tags","description":"ๅˆ†็ฑปๆ ‡็ญพ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(64)","constraints":"NOT NULL, UNIQUE"},{"name":"color","type":"VARCHAR(7)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_tags_user ON tags(user_id)"]},{"table":"comments","description":"่ฏ„่ฎบ็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"entity_type","type":"VARCHAR(32)","constraints":"NOT NULL"},{"name":"entity_id","type":"UUID","constraints":"NOT NULL"},{"name":"content","type":"TEXT","constraints":"NOT NULL"},{"name":"parent_id","type":"UUID","constraints":"FK โ†’ comments.id (่‡ชๅผ•็”จ)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_comments_user ON comments(user_id)"]},{"table":"media","description":"ๅช’ไฝ“ๅบ“่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"filename","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"url","type":"TEXT","constraints":"NOT NULL"},{"name":"mime_type","type":"VARCHAR(64)"},{"name":"size_bytes","type":"INTEGER"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_media_user ON media(user_id)"]}],"apiDesign":[{"resource":"articles","basePath":"/api/articles","endpoints":[{"method":"GET","path":"/api/articles","description":"่Žทๅ–ๆ–‡็ซ ๅ‘ๅธƒๅˆ—่กจ"},{"method":"POST","path":"/api/articles","description":"ๅˆ›ๅปบๆ–‡็ซ ๅ‘ๅธƒ"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}]},{"resource":"comments","basePath":"/api/comments","endpoints":[{"method":"GET","path":"/api/comments","description":"่Žทๅ–่ฏ„่ฎบ็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/comments","description":"ๅˆ›ๅปบ่ฏ„่ฎบ็ฎก็†"}]},{"resource":"media","basePath":"/api/media","endpoints":[{"method":"GET","path":"/api/media","description":"่Žทๅ–ๅช’ไฝ“ๅบ“ๅˆ—่กจ"},{"method":"POST","path":"/api/media","description":"ๅˆ›ๅปบๅช’ไฝ“ๅบ“"}]},{"resource":"tags","basePath":"/api/tags","endpoints":[{"method":"GET","path":"/api/tags","description":"่Žทๅ–ๅˆ†็ฑปๆ ‡็ญพๅˆ—่กจ"},{"method":"POST","path":"/api/tags","description":"ๅˆ›ๅปบๅˆ†็ฑปๆ ‡็ญพ"}]}],"directoryStructure":["socialapp/","โ”œโ”€โ”€ apps/","โ”‚ โ”œโ”€โ”€ mobile/ # React Native / Flutter ็งปๅŠจ็ซฏ","โ”‚ โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # ้กต้ข็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆ–‡็ซ ๅ‘ๅธƒ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆ†็ฑปๆ ‡็ญพ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ content/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅช’ไฝ“ๅบ“/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ้€š็”จ็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # ่‡ชๅฎšไน‰ Hooks","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API ่ฐƒ็”จๅฑ‚","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ # ็Šถๆ€็ฎก็†","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ","โ”‚ โ”‚ โ”œโ”€โ”€ app.json","โ”‚ โ”‚ โ””โ”€โ”€ package.json","โ”‚ โ””โ”€โ”€ web/ # Web ็ฎก็†ๅŽๅฐ","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ pages/","โ”‚ โ”‚ โ”œโ”€โ”€ components/","โ”‚ โ”‚ โ””โ”€โ”€ layouts/","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ packages/","โ”‚ โ””โ”€โ”€ shared/ # ๅ…ฑไบซ็ฑปๅž‹/ๅธธ้‡","โ”œโ”€โ”€ server/ # NestJS ๅŽ็ซฏๆœๅŠก","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ modules/ # ไธšๅŠกๆจกๅ—","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆ–‡็ซ ๅ‘ๅธƒ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆ–‡็ซ ๅ‘ๅธƒ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆ–‡็ซ ๅ‘ๅธƒ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๆ–‡็ซ ๅ‘ๅธƒ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆ†็ฑปๆ ‡็ญพ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆ†็ฑปๆ ‡็ญพ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆ†็ฑปๆ ‡็ญพ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆ†็ฑปๆ ‡็ญพ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ content/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ content.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ content.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ content.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅช’ไฝ“ๅบ“/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅช’ไฝ“ๅบ“.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅช’ไฝ“ๅบ“.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅช’ไฝ“ๅบ“.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”œโ”€โ”€ common/ # ้€š็”จ๏ผˆguards, filters, interceptors๏ผ‰","โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ # Prisma ORM","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ schema.prisma","โ”‚ โ”‚ โ”œโ”€โ”€ config/ # ็Žฏๅขƒ้…็ฝฎ","โ”‚ โ”‚ โ””โ”€โ”€ main.ts","โ”‚ โ”œโ”€โ”€ test/","โ”‚ โ”œโ”€โ”€ Dockerfile","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ docker-compose.yml","โ”œโ”€โ”€ .github/workflows/ # CI/CD","โ”‚ โ””โ”€โ”€ ci.yml","โ”œโ”€โ”€ .env.example","โ”œโ”€โ”€ README.md","โ””โ”€โ”€ turbo.json # Monorepo ้…็ฝฎ"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions โ†’ Build โ†’ Test โ†’ Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:35.601Z","sourceDomain":"social","moduleCount":4,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/blog-cms/backend/.gitignore b/.benchmark/blog-cms/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/blog-cms/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/blog-cms/backend/README.md b/.benchmark/blog-cms/backend/README.md new file mode 100644 index 0000000..945cc2f --- /dev/null +++ b/.benchmark/blog-cms/backend/README.md @@ -0,0 +1,118 @@ +# SocialApp โ€” Backend API + +> ไธ€ๆฌพไปฅๅ†…ๅฎนๅˆ†ไบซๅ’Œ็”จๆˆทไบ’ๅŠจไธบๆ ธๅฟƒ็š„็คพไบคๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### articles +- `GET /api/articles` โ€” List all +- `GET /api/articles/:id` โ€” Get by ID +- `POST /api/articles` โ€” Create +- `PUT /api/articles/:id` โ€” Update +- `DELETE /api/articles/:id` โ€” Delete + +#### tags +- `GET /api/tags` โ€” List all +- `GET /api/tags/:id` โ€” Get by ID +- `POST /api/tags` โ€” Create +- `PUT /api/tags/:id` โ€” Update +- `DELETE /api/tags/:id` โ€” Delete + +#### comments +- `GET /api/comments` โ€” List all +- `GET /api/comments/:id` โ€” Get by ID +- `POST /api/comments` โ€” Create +- `PUT /api/comments/:id` โ€” Update +- `DELETE /api/comments/:id` โ€” Delete + +#### media +- `GET /api/media` โ€” List all +- `GET /api/media/:id` โ€” Get by ID +- `POST /api/media` โ€” Create +- `PUT /api/media/:id` โ€” Update +- `DELETE /api/media/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/blog-cms/backend/package.json b/.benchmark/blog-cms/backend/package.json new file mode 100644 index 0000000..61e5ea4 --- /dev/null +++ b/.benchmark/blog-cms/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/backend/src/__tests__/approvals.test.ts b/.benchmark/blog-cms/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/articles.test.ts b/.benchmark/blog-cms/backend/src/__tests__/articles.test.ts new file mode 100644 index 0000000..efd81e7 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/articles.test.ts @@ -0,0 +1,123 @@ +// Articles CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "content": "sample-content", + "excerpt": "sample-excerpt", + "coverUrl": "sample-coverurl", + "authorId": "sample-authorid", + "category": "sample-category", + "publishedAt": "sample-publishedat" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/articles", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/articles", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/articles", () => { + it("creates a article", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/articles", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/articles/:id", () => { + it("returns the created article", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/articles/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/articles/:id", () => { + it("updates the article", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/articles/:id", () => { + it("deletes the article", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/articles/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/auth.test.ts b/.benchmark/blog-cms/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/comments.test.ts b/.benchmark/blog-cms/backend/src/__tests__/comments.test.ts new file mode 100644 index 0000000..9e3145d --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/comments.test.ts @@ -0,0 +1,120 @@ +// Comments CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "content": "sample-content", + "parentId": "sample-parentid" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/comments", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/comments", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/comments", () => { + it("creates a comment", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/comments", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/comments/:id", () => { + it("returns the created comment", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/comments/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/comments/:id", () => { + it("updates the comment", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/comments/:id", () => { + it("deletes the comment", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/comments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/contracts.test.ts b/.benchmark/blog-cms/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/customers.test.ts b/.benchmark/blog-cms/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/items.test.ts b/.benchmark/blog-cms/backend/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/media.test.ts b/.benchmark/blog-cms/backend/src/__tests__/media.test.ts new file mode 100644 index 0000000..9c5188b --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/media.test.ts @@ -0,0 +1,120 @@ +// Media CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "filename": "sample-filename", + "url": "sample-url", + "mimeType": "sample-mimetype", + "sizeBytes": 1 + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/media", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/media", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/media", () => { + it("creates a media", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/media", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/media/:id", () => { + it("returns the created media", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/media/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/media/:id", () => { + it("updates the media", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/media/:id", () => { + it("deletes the media", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/media/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/reminders.test.ts b/.benchmark/blog-cms/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/tags.test.ts b/.benchmark/blog-cms/backend/src/__tests__/tags.test.ts new file mode 100644 index 0000000..dc00d1f --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/tags.test.ts @@ -0,0 +1,118 @@ +// Tags CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "color": "sample-color" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/tags", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/tags", () => { + it("creates a tag", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/tags/:id", () => { + it("returns the created tag", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/tags/:id", () => { + it("updates the tag", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/tags/:id", () => { + it("deletes the tag", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/__tests__/users.test.ts b/.benchmark/blog-cms/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/blog-cms/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/backend/src/db/client.ts b/.benchmark/blog-cms/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/blog-cms/backend/src/db/schema.ts b/.benchmark/blog-cms/backend/src/db/schema.ts new file mode 100644 index 0000000..c1e9b27 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS articles (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n content TEXT,\n excerpt TEXT,\n cover_url TEXT,\n status TEXT,\n author_id TEXT REFERENCES authors(id),\n category TEXT,\n published_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS tags (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL UNIQUE,\n color TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n content TEXT NOT NULL,\n parent_id TEXT REFERENCES parents(id),\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS media (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n filename TEXT NOT NULL,\n url TEXT NOT NULL,\n mime_type TEXT,\n size_bytes INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/blog-cms/backend/src/index.ts b/.benchmark/blog-cms/backend/src/index.ts new file mode 100644 index 0000000..b663dcf --- /dev/null +++ b/.benchmark/blog-cms/backend/src/index.ts @@ -0,0 +1,79 @@ +// SocialApp โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { articlesRoutes } from "./routes/articles.js"; +import { tagsRoutes } from "./routes/tags.js"; +import { commentsRoutes } from "./routes/comments.js"; +import { mediaRoutes } from "./routes/media.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-5f616668"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(articlesRoutes, { prefix: "/api/articles" }); + await app.register(tagsRoutes, { prefix: "/api/tags" }); + await app.register(commentsRoutes, { prefix: "/api/comments" }); + await app.register(mediaRoutes, { prefix: "/api/media" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/blog-cms/backend/src/middleware/auth.ts b/.benchmark/blog-cms/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/blog-cms/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/blog-cms/backend/src/routes/approvals.ts b/.benchmark/blog-cms/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals โ€” create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/articles.ts b/.benchmark/blog-cms/backend/src/routes/articles.ts new file mode 100644 index 0000000..5a2ee89 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/articles.ts @@ -0,0 +1,51 @@ +// Auto-generated Articles routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ArticlesService } from "../services/article.js"; +import type { CreateArticlesInput, UpdateArticlesInput } from "../types/index.js"; + +const service = new ArticlesService(); + +export async function articlesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/articles โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/articles/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Articles not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/articles โ€” create + app.post<{ Body: CreateArticlesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/articles/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateArticlesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Articles not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/articles/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Articles not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/auth.ts b/.benchmark/blog-cms/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/comments.ts b/.benchmark/blog-cms/backend/src/routes/comments.ts new file mode 100644 index 0000000..fe2824f --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/comments.ts @@ -0,0 +1,51 @@ +// Auto-generated Comments routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CommentsService } from "../services/comment.js"; +import type { CreateCommentsInput, UpdateCommentsInput } from "../types/index.js"; + +const service = new CommentsService(); + +export async function commentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/comments โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/comments/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Comments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/comments โ€” create + app.post<{ Body: CreateCommentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/comments/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCommentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Comments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/comments/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Comments not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/contracts.ts b/.benchmark/blog-cms/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts โ€” create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/customers.ts b/.benchmark/blog-cms/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers โ€” create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/items.ts b/.benchmark/blog-cms/backend/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items โ€” create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/media.ts b/.benchmark/blog-cms/backend/src/routes/media.ts new file mode 100644 index 0000000..b3ec363 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/media.ts @@ -0,0 +1,51 @@ +// Auto-generated Media routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { MediaService } from "../services/media.js"; +import type { CreateMediaInput, UpdateMediaInput } from "../types/index.js"; + +const service = new MediaService(); + +export async function mediaRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/media โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/media/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Media not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/media โ€” create + app.post<{ Body: CreateMediaInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/media/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateMediaInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Media not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/media/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Media not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/reminders.ts b/.benchmark/blog-cms/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders โ€” create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/tags.ts b/.benchmark/blog-cms/backend/src/routes/tags.ts new file mode 100644 index 0000000..511a348 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/tags.ts @@ -0,0 +1,51 @@ +// Auto-generated Tags routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { TagsService } from "../services/tag.js"; +import type { CreateTagsInput, UpdateTagsInput } from "../types/index.js"; + +const service = new TagsService(); + +export async function tagsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/tags โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/tags/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tags not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/tags โ€” create + app.post<{ Body: CreateTagsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/tags/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateTagsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tags not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/tags/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Tags not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/routes/users.ts b/.benchmark/blog-cms/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/backend/src/services/approval.ts b/.benchmark/blog-cms/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/article.ts b/.benchmark/blog-cms/backend/src/services/article.ts new file mode 100644 index 0000000..500ae9e --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/article.ts @@ -0,0 +1,58 @@ +// Auto-generated Articles service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Articles, CreateArticlesInput, UpdateArticlesInput } from "../types/index.js"; + +export class ArticlesService { + /** List all articles */ + list(): Articles[] { + return queryAll("SELECT * FROM articles ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Articles | undefined { + return queryOne("SELECT * FROM articles WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateArticlesInput): Articles { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "content", "excerpt", "cover_url", "author_id", "category", "published_at", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.content ?? null, input.excerpt ?? null, input.coverUrl ?? null, input.authorId ?? null, input.category ?? null, input.publishedAt ?? null, now, now]; + + execute(`INSERT INTO articles (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateArticlesInput): Articles | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.excerpt !== undefined) { sets.push("excerpt = ?"); values.push(input.excerpt); } + if (input.coverUrl !== undefined) { sets.push("cover_url = ?"); values.push(input.coverUrl); } + if (input.authorId !== undefined) { sets.push("author_id = ?"); values.push(input.authorId); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + if (input.publishedAt !== undefined) { sets.push("published_at = ?"); values.push(input.publishedAt); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE articles SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM articles WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/comment.ts b/.benchmark/blog-cms/backend/src/services/comment.ts new file mode 100644 index 0000000..fee428e --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/comment.ts @@ -0,0 +1,55 @@ +// Auto-generated Comments service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Comments, CreateCommentsInput, UpdateCommentsInput } from "../types/index.js"; + +export class CommentsService { + /** List all comments */ + list(): Comments[] { + return queryAll("SELECT * FROM comments ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Comments | undefined { + return queryOne("SELECT * FROM comments WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCommentsInput): Comments { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "content", "parent_id", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.content ?? null, input.parentId ?? null, now, now]; + + execute(`INSERT INTO comments (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCommentsInput): Comments | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.parentId !== undefined) { sets.push("parent_id = ?"); values.push(input.parentId); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE comments SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM comments WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/contract.ts b/.benchmark/blog-cms/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/customer.ts b/.benchmark/blog-cms/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/item.ts b/.benchmark/blog-cms/backend/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/media.ts b/.benchmark/blog-cms/backend/src/services/media.ts new file mode 100644 index 0000000..6a43a00 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/media.ts @@ -0,0 +1,55 @@ +// Auto-generated Media service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Media, CreateMediaInput, UpdateMediaInput } from "../types/index.js"; + +export class MediaService { + /** List all media */ + list(): Media[] { + return queryAll("SELECT * FROM media ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Media | undefined { + return queryOne("SELECT * FROM media WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateMediaInput): Media { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "filename", "url", "mime_type", "size_bytes", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.filename ?? null, input.url ?? null, input.mimeType ?? null, input.sizeBytes ?? null, now, now]; + + execute(`INSERT INTO media (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateMediaInput): Media | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.filename !== undefined) { sets.push("filename = ?"); values.push(input.filename); } + if (input.url !== undefined) { sets.push("url = ?"); values.push(input.url); } + if (input.mimeType !== undefined) { sets.push("mime_type = ?"); values.push(input.mimeType); } + if (input.sizeBytes !== undefined) { sets.push("size_bytes = ?"); values.push(input.sizeBytes); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE media SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM media WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/reminder.ts b/.benchmark/blog-cms/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/tag.ts b/.benchmark/blog-cms/backend/src/services/tag.ts new file mode 100644 index 0000000..073aa39 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/tag.ts @@ -0,0 +1,53 @@ +// Auto-generated Tags service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Tags, CreateTagsInput, UpdateTagsInput } from "../types/index.js"; + +export class TagsService { + /** List all tags */ + list(): Tags[] { + return queryAll("SELECT * FROM tags ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Tags | undefined { + return queryOne("SELECT * FROM tags WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateTagsInput): Tags { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "color", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.color ?? null, now, now]; + + execute(`INSERT INTO tags (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateTagsInput): Tags | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE tags SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM tags WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/services/user.ts b/.benchmark/blog-cms/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/blog-cms/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/backend/src/types/fastify.d.ts b/.benchmark/blog-cms/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/blog-cms/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/blog-cms/backend/src/types/index.ts b/.benchmark/blog-cms/backend/src/types/index.ts new file mode 100644 index 0000000..22919a9 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/types/index.ts @@ -0,0 +1,320 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateArticlesInput { + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface CreateTagsInput { + userId?: string; + name: string; + color?: string; +} + +export interface CreateCommentsInput { + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; +} + +export interface CreateMediaInput { + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateArticlesInput { + userId?: string; + title?: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface UpdateTagsInput { + userId?: string; + name?: string; + color?: string; +} + +export interface UpdateCommentsInput { + userId?: string; + entityType?: string; + entityId?: string; + content?: string; + parentId?: string; +} + +export interface UpdateMediaInput { + userId?: string; + filename?: string; + url?: string; + mimeType?: string; + sizeBytes?: number; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/blog-cms/backend/src/types/sql.js.d.ts b/.benchmark/blog-cms/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/blog-cms/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/blog-cms/backend/tsconfig.json b/.benchmark/blog-cms/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/blog-cms/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/electron/README.md b/.benchmark/blog-cms/electron/README.md new file mode 100644 index 0000000..c65d8f8 --- /dev/null +++ b/.benchmark/blog-cms/electron/README.md @@ -0,0 +1,82 @@ +# socialapp โ€” Desktop + +> Electron desktop application wrapping the socialapp fullstack web project. + +## Architecture + +``` +Web Fullstack Project + ร— + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** โ€” Window management, menu, tray, IPC, auto-update +- **Preload** โ€” Secure bridge between main and renderer +- **Renderer** โ€” Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +โ”œโ”€โ”€ main.ts โ€” Main process (BrowserWindow, Menu, lifecycle) +โ”œโ”€โ”€ preload.ts โ€” Secure IPC bridge (contextBridge) +โ”œโ”€โ”€ ipc.ts โ€” IPC handlers (dialogs, store, notifications) +โ”œโ”€โ”€ tray.ts โ€” System tray +โ”œโ”€โ”€ updater.ts โ€” Auto-update (electron-updater) +โ””โ”€โ”€ utils.ts โ€” Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/blog-cms/electron/assets/icon.png b/.benchmark/blog-cms/electron/assets/icon.png new file mode 100644 index 0000000..554e8a3 Binary files /dev/null and b/.benchmark/blog-cms/electron/assets/icon.png differ diff --git a/.benchmark/blog-cms/electron/electron-builder.yml b/.benchmark/blog-cms/electron/electron-builder.yml new file mode 100644 index 0000000..bb40cff --- /dev/null +++ b/.benchmark/blog-cms/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml โ€” Auto-generated by SF-06 + +appId: com.socialapp.desktop +productName: socialapp +copyright: "Copyright ยฉ 2026 socialapp" + +# โ”€โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +directories: + output: release + buildResources: assets + +# โ”€โ”€โ”€ Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# โ”€โ”€โ”€ Extra Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# โ”€โ”€โ”€ macOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# โ”€โ”€โ”€ Windows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: socialapp + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: socialapp + +# โ”€โ”€โ”€ Linux โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: socialapp + Comment: socialapp Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# โ”€โ”€โ”€ Auto Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +publish: + provider: generic + url: https://releases.example.com/socialapp/ + channel: latest diff --git a/.benchmark/blog-cms/electron/electron/ipc.ts b/.benchmark/blog-cms/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // โ”€โ”€ App Info โ”€โ”€ + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // โ”€โ”€ Window Controls โ”€โ”€ + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // โ”€โ”€ File Dialogs โ”€โ”€ + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // โ”€โ”€ Notifications โ”€โ”€ + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // โ”€โ”€ Shell โ”€โ”€ + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // โ”€โ”€ Persistent Store โ”€โ”€ + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/blog-cms/electron/electron/main.ts b/.benchmark/blog-cms/electron/electron/main.ts new file mode 100644 index 0000000..9333acb --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "socialapp"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// โ”€โ”€โ”€ Create Window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // โ”€โ”€ Load content โ”€โ”€ + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // โ”€โ”€ Show when ready โ”€โ”€ + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // โ”€โ”€ External links โ†’ default browser โ”€โ”€ + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // โ”€โ”€ Prevent navigation away โ”€โ”€ + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// โ”€โ”€โ”€ Icon Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// โ”€โ”€โ”€ API Server (Production) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// โ”€โ”€โ”€ Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// โ”€โ”€โ”€ App Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed โ€” continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/blog-cms/electron/electron/preload.ts b/.benchmark/blog-cms/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// โ”€โ”€โ”€ Expose safe API to renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +contextBridge.exposeInMainWorld("electronAPI", { + // โ”€โ”€ App Info โ”€โ”€ + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // โ”€โ”€ Window Controls โ”€โ”€ + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // โ”€โ”€ File Dialogs โ”€โ”€ + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // โ”€โ”€ Notifications โ”€โ”€ + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // โ”€โ”€ Shell โ”€โ”€ + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // โ”€โ”€ Store โ”€โ”€ + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // โ”€โ”€ Updates โ”€โ”€ + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // โ”€โ”€ Update Events โ”€โ”€ + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // โ”€โ”€ Remove listener โ”€โ”€ + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// โ”€โ”€โ”€ Type declarations for renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/blog-cms/electron/electron/tray.ts b/.benchmark/blog-cms/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/blog-cms/electron/electron/updater.ts b/.benchmark/blog-cms/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/blog-cms/electron/electron/utils.ts b/.benchmark/blog-cms/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/blog-cms/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// โ”€โ”€โ”€ Dev mode detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function isDev(): boolean { + return !app.isPackaged; +} + +// โ”€โ”€โ”€ Web URL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// โ”€โ”€โ”€ API Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// โ”€โ”€โ”€ Get API port from env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/blog-cms/electron/entitlements.mac.plist b/.benchmark/blog-cms/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/blog-cms/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/blog-cms/electron/package.json b/.benchmark/blog-cms/electron/package.json new file mode 100644 index 0000000..47866c3 --- /dev/null +++ b/.benchmark/blog-cms/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "socialapp-desktop", + "version": "1.0.0", + "private": true, + "description": "socialapp โ€” Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "socialapp", + "appId": "com.socialapp.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/electron/tsconfig.json b/.benchmark/blog-cms/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/blog-cms/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/frontend/app/articles/page.tsx b/.benchmark/blog-cms/frontend/app/articles/page.tsx new file mode 100644 index 0000000..b3111b3 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/articles/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useArticles } from "@/hooks/useArticles"; + +export default function PagePage() { + const { data, loading, error, refetch } = useArticles(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆ–‡็ซ ๅ‘ๅธƒ

+

ๆ–‡็ซ ๅ‘ๅธƒ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/comments/page.tsx b/.benchmark/blog-cms/frontend/app/comments/page.tsx new file mode 100644 index 0000000..0ed16e3 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/comments/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useComments } from "@/hooks/useComments"; + +export default function PagePage() { + const { data, loading, error, refetch } = useComments(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

่ฏ„่ฎบ็ฎก็†

+

่ฏ„่ฎบ็ฎก็†

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/globals.css b/.benchmark/blog-cms/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/blog-cms/frontend/app/layout.tsx b/.benchmark/blog-cms/frontend/app/layout.tsx new file mode 100644 index 0000000..3613994 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "SocialApp", + description: "ไธ€ๆฌพไปฅๅ†…ๅฎนๅˆ†ไบซๅ’Œ็”จๆˆทไบ’ๅŠจไธบๆ ธๅฟƒ็š„็คพไบคๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/blog-cms/frontend/app/media/page.tsx b/.benchmark/blog-cms/frontend/app/media/page.tsx new file mode 100644 index 0000000..8297264 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/media/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useMedia } from "@/hooks/useMedia"; + +export default function PagePage() { + const { data, loading, error, refetch } = useMedia(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅช’ไฝ“ๅบ“

+

ๅช’ไฝ“ๅบ“

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/messages/page.tsx b/.benchmark/blog-cms/frontend/app/messages/page.tsx new file mode 100644 index 0000000..bd9a20e --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/messages/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆถˆๆฏ

+

็งไฟกๅˆ—่กจใ€่Šๅคฉ

+
+ +
+ + { +

ๆถˆๆฏ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /messages

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/page.tsx b/.benchmark/blog-cms/frontend/app/page.tsx new file mode 100644 index 0000000..eb4a2cc --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

SocialApp

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ๆ–‡็ซ ๅ‘ๅธƒ + + + ๐Ÿ“… + ๅˆ†็ฑปๆ ‡็ญพ + + + ๐Ÿ“ + ่ฏ„่ฎบ็ฎก็† + + + ๐Ÿ“ธ + ๅช’ไฝ“ๅบ“ + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ SocialApp

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx b/.benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx new file mode 100644 index 0000000..3933192 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/profile/[userId]/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ไธชไบบ

+

ไธชไบบไธป้กต

+
+ +
+ + { +

ไธชไบบ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile/:userId

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/publish/page.tsx b/.benchmark/blog-cms/frontend/app/publish/page.tsx new file mode 100644 index 0000000..e09151b --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/publish/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅ‘ๅธƒ

+

็ผ–่พ‘ๅ™จใ€็›ธๅ†Œ้€‰ๆ‹ฉ

+
+ +
+ + { +

ๅ‘ๅธƒ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /publish

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/tags/page.tsx b/.benchmark/blog-cms/frontend/app/tags/page.tsx new file mode 100644 index 0000000..c9d5031 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/tags/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useTags } from "@/hooks/useTags"; + +export default function PagePage() { + const { data, loading, error, refetch } = useTags(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅˆ†็ฑปๆ ‡็ญพ

+

ๅˆ†็ฑปๆ ‡็ญพ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/ๅˆ†็ฑปๆ ‡็ญพ/page.tsx b/.benchmark/blog-cms/frontend/app/ๅˆ†็ฑปๆ ‡็ญพ/page.tsx new file mode 100644 index 0000000..64f5c2a --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/ๅˆ†็ฑปๆ ‡็ญพ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๅˆ†็ฑปๆ ‡็ญพ } from "@/hooks/useๅˆ†็ฑปๆ ‡็ญพ"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๅˆ†็ฑปๆ ‡็ญพ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅˆ†็ฑปๆ ‡็ญพ

+

ๅˆ†็ฑปๆ ‡็ญพ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/ๅช’ไฝ“ๅบ“/page.tsx b/.benchmark/blog-cms/frontend/app/ๅช’ไฝ“ๅบ“/page.tsx new file mode 100644 index 0000000..dd0093f --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/ๅช’ไฝ“ๅบ“/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๅช’ไฝ“ๅบ“ } from "@/hooks/useๅช’ไฝ“ๅบ“"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๅช’ไฝ“ๅบ“(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅช’ไฝ“ๅบ“

+

ๅช’ไฝ“ๅบ“

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/ๆ–‡็ซ ๅ‘ๅธƒ/page.tsx b/.benchmark/blog-cms/frontend/app/ๆ–‡็ซ ๅ‘ๅธƒ/page.tsx new file mode 100644 index 0000000..577a7ca --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/ๆ–‡็ซ ๅ‘ๅธƒ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๆ–‡็ซ ๅ‘ๅธƒ } from "@/hooks/useๆ–‡็ซ ๅ‘ๅธƒ"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๆ–‡็ซ ๅ‘ๅธƒ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆ–‡็ซ ๅ‘ๅธƒ

+

ๆ–‡็ซ ๅ‘ๅธƒ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/app/่ฏ„่ฎบ็ฎก็†/page.tsx b/.benchmark/blog-cms/frontend/app/่ฏ„่ฎบ็ฎก็†/page.tsx new file mode 100644 index 0000000..eca5fb4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/app/่ฏ„่ฎบ็ฎก็†/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use่ฏ„่ฎบ็ฎก็† } from "@/hooks/use่ฏ„่ฎบ็ฎก็†"; + +export default function PagePage() { + const { data, loading, error, refetch } = use่ฏ„่ฎบ็ฎก็†(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

่ฏ„่ฎบ็ฎก็†

+

่ฏ„่ฎบ็ฎก็†

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/layout/BottomNav.tsx b/.benchmark/blog-cms/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/frontend/components/layout/Sidebar.tsx b/.benchmark/blog-cms/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Button.tsx b/.benchmark/blog-cms/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Card.tsx b/.benchmark/blog-cms/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/EmptyState.tsx b/.benchmark/blog-cms/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Input.tsx b/.benchmark/blog-cms/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/blog-cms/frontend/components/ui/Modal.tsx b/.benchmark/blog-cms/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/blog-cms/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/blog-cms/frontend/hooks/useArticles.ts b/.benchmark/blog-cms/frontend/hooks/useArticles.ts new file mode 100644 index 0000000..17a123b --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useArticles.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for articles +type Article = Record; + +/** + * Fetch and manage articles data. + */ +export function useArticles() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/articles`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useAuth.ts b/.benchmark/blog-cms/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useComments.ts b/.benchmark/blog-cms/frontend/hooks/useComments.ts new file mode 100644 index 0000000..b6bfd57 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useComments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for comments +type Comment = Record; + +/** + * Fetch and manage comments data. + */ +export function useComments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/comments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useDebounce.ts b/.benchmark/blog-cms/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useFeed.ts b/.benchmark/blog-cms/frontend/hooks/useFeed.ts new file mode 100644 index 0000000..18364d8 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useFeed.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for feed +type Feed = Record; + +/** + * Fetch and manage feed data. + */ +export function useFeed() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/feed`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useForm.ts b/.benchmark/blog-cms/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useItem.ts b/.benchmark/blog-cms/frontend/hooks/useItem.ts new file mode 100644 index 0000000..afd3121 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for ่ฏ„่ฎบ็ฎก็† +type Item = Record; + +/** + * Fetch and manage ่ฏ„่ฎบ็ฎก็† data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/่ฏ„่ฎบ็ฎก็†`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useMedia.ts b/.benchmark/blog-cms/frontend/hooks/useMedia.ts new file mode 100644 index 0000000..9f8e38f --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useMedia.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for media +type Media = Record; + +/** + * Fetch and manage media data. + */ +export function useMedia() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/media`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/usePosts.ts b/.benchmark/blog-cms/frontend/hooks/usePosts.ts new file mode 100644 index 0000000..6fa9aae --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/usePosts.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for posts +type Post = Record; + +/** + * Fetch and manage posts data. + */ +export function usePosts() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/posts`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useTags.ts b/.benchmark/blog-cms/frontend/hooks/useTags.ts new file mode 100644 index 0000000..ff0d19b --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useTags.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for tags +type Tag = Record; + +/** + * Fetch and manage tags data. + */ +export function useTags() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/tags`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/hooks/useUsers.ts b/.benchmark/blog-cms/frontend/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/blog-cms/frontend/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/frontend/next-env.d.ts b/.benchmark/blog-cms/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/blog-cms/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/blog-cms/frontend/next.config.ts b/.benchmark/blog-cms/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/blog-cms/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/blog-cms/frontend/package.json b/.benchmark/blog-cms/frontend/package.json new file mode 100644 index 0000000..68e4bcf --- /dev/null +++ b/.benchmark/blog-cms/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/frontend/postcss.config.js b/.benchmark/blog-cms/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/blog-cms/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/blog-cms/frontend/services/api.ts b/.benchmark/blog-cms/frontend/services/api.ts new file mode 100644 index 0000000..959cc20 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for SocialApp + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/blog-cms/frontend/services/articles.ts b/.benchmark/blog-cms/frontend/services/articles.ts new file mode 100644 index 0000000..ca7cee4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/articles.ts @@ -0,0 +1,10 @@ +// Auto-generated articles service +import { api } from "./api"; + +export function get() { + return api.get("/api/articles"); +} + +export function post(data: Record) { + return api.post("/api/articles", data); +} diff --git a/.benchmark/blog-cms/frontend/services/auth.ts b/.benchmark/blog-cms/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/blog-cms/frontend/services/comments.ts b/.benchmark/blog-cms/frontend/services/comments.ts new file mode 100644 index 0000000..7df69f4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/comments.ts @@ -0,0 +1,10 @@ +// Auto-generated comments service +import { api } from "./api"; + +export function get() { + return api.get("/api/comments"); +} + +export function post(data: Record) { + return api.post("/api/comments", data); +} diff --git a/.benchmark/blog-cms/frontend/services/feed.ts b/.benchmark/blog-cms/frontend/services/feed.ts new file mode 100644 index 0000000..85b119b --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/feed.ts @@ -0,0 +1,6 @@ +// Auto-generated feed service +import { api } from "./api"; + +export function get() { + return api.get("/api/feed"); +} diff --git a/.benchmark/blog-cms/frontend/services/media.ts b/.benchmark/blog-cms/frontend/services/media.ts new file mode 100644 index 0000000..5d7297c --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/media.ts @@ -0,0 +1,10 @@ +// Auto-generated media service +import { api } from "./api"; + +export function get() { + return api.get("/api/media"); +} + +export function post(data: Record) { + return api.post("/api/media", data); +} diff --git a/.benchmark/blog-cms/frontend/services/posts.ts b/.benchmark/blog-cms/frontend/services/posts.ts new file mode 100644 index 0000000..6aeb9b7 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/posts.ts @@ -0,0 +1,10 @@ +// Auto-generated posts service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/posts", data); +} + +export function post_id_like(data: Record) { + return api.post("/api/posts", data); +} diff --git a/.benchmark/blog-cms/frontend/services/tags.ts b/.benchmark/blog-cms/frontend/services/tags.ts new file mode 100644 index 0000000..7d52e35 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/tags.ts @@ -0,0 +1,10 @@ +// Auto-generated tags service +import { api } from "./api"; + +export function get() { + return api.get("/api/tags"); +} + +export function post(data: Record) { + return api.post("/api/tags", data); +} diff --git a/.benchmark/blog-cms/frontend/services/users.ts b/.benchmark/blog-cms/frontend/services/users.ts new file mode 100644 index 0000000..4b573b4 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/users.ts @@ -0,0 +1,6 @@ +// Auto-generated users service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/blog-cms/frontend/services/ๅˆ†็ฑปๆ ‡็ญพ.ts b/.benchmark/blog-cms/frontend/services/ๅˆ†็ฑปๆ ‡็ญพ.ts new file mode 100644 index 0000000..91394a3 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/ๅˆ†็ฑปๆ ‡็ญพ.ts @@ -0,0 +1,10 @@ +// Auto-generated ๅˆ†็ฑปๆ ‡็ญพ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๅˆ†็ฑปๆ ‡็ญพ"); +} + +export function post(data: Record) { + return api.post("/api/ๅˆ†็ฑปๆ ‡็ญพ", data); +} diff --git a/.benchmark/blog-cms/frontend/services/ๅช’ไฝ“ๅบ“.ts b/.benchmark/blog-cms/frontend/services/ๅช’ไฝ“ๅบ“.ts new file mode 100644 index 0000000..c880489 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/ๅช’ไฝ“ๅบ“.ts @@ -0,0 +1,10 @@ +// Auto-generated ๅช’ไฝ“ๅบ“ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๅช’ไฝ“ๅบ“"); +} + +export function post(data: Record) { + return api.post("/api/ๅช’ไฝ“ๅบ“", data); +} diff --git a/.benchmark/blog-cms/frontend/services/ๆ–‡็ซ ๅ‘ๅธƒ.ts b/.benchmark/blog-cms/frontend/services/ๆ–‡็ซ ๅ‘ๅธƒ.ts new file mode 100644 index 0000000..903f3d5 --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/ๆ–‡็ซ ๅ‘ๅธƒ.ts @@ -0,0 +1,10 @@ +// Auto-generated ๆ–‡็ซ ๅ‘ๅธƒ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๆ–‡็ซ ๅ‘ๅธƒ"); +} + +export function post(data: Record) { + return api.post("/api/ๆ–‡็ซ ๅ‘ๅธƒ", data); +} diff --git a/.benchmark/blog-cms/frontend/services/่ฏ„่ฎบ็ฎก็†.ts b/.benchmark/blog-cms/frontend/services/่ฏ„่ฎบ็ฎก็†.ts new file mode 100644 index 0000000..f96682d --- /dev/null +++ b/.benchmark/blog-cms/frontend/services/่ฏ„่ฎบ็ฎก็†.ts @@ -0,0 +1,10 @@ +// Auto-generated ่ฏ„่ฎบ็ฎก็† service +import { api } from "./api"; + +export function get() { + return api.get("/api/่ฏ„่ฎบ็ฎก็†"); +} + +export function post(data: Record) { + return api.post("/api/่ฏ„่ฎบ็ฎก็†", data); +} diff --git a/.benchmark/blog-cms/frontend/tailwind.config.ts b/.benchmark/blog-cms/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/blog-cms/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/blog-cms/frontend/tsconfig.json b/.benchmark/blog-cms/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/blog-cms/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/frontend/types/index.ts b/.benchmark/blog-cms/frontend/types/index.ts new file mode 100644 index 0000000..0f93fab --- /dev/null +++ b/.benchmark/blog-cms/frontend/types/index.ts @@ -0,0 +1,130 @@ +// Auto-generated types for SocialApp + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/.gitignore b/.benchmark/blog-cms/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/blog-cms/fullstack/README.md b/.benchmark/blog-cms/fullstack/README.md new file mode 100644 index 0000000..6341f36 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/README.md @@ -0,0 +1,125 @@ +# SocialApp โ€” Fullstack Project + +> ไธ€ๆฌพไปฅๅ†…ๅฎนๅˆ†ไบซๅ’Œ็”จๆˆทไบ’ๅŠจไธบๆ ธๅฟƒ็š„็คพไบคๅบ”็”จใ€‚ + +## Architecture + +``` +apps/ +โ”œโ”€โ”€ web/ # Next.js 15 + TypeScript + Tailwind CSS +โ””โ”€โ”€ api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +โ”œโ”€โ”€ shared-types/ # @shared/types โ€” shared TypeScript interfaces +โ””โ”€โ”€ shared-config/ # @shared/config โ€” shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` โ€” List +- `GET /api/users/:id` โ€” Get +- `POST /api/users` โ€” Create +- `PUT /api/users/:id` โ€” Update +- `DELETE /api/users/:id` โ€” Delete + +#### contracts +- `GET /api/contracts` โ€” List +- `GET /api/contracts/:id` โ€” Get +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List +- `GET /api/approvals/:id` โ€” Get +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List +- `GET /api/customers/:id` โ€” Get +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List +- `GET /api/reminders/:id` โ€” Get +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### articles +- `GET /api/articles` โ€” List +- `GET /api/articles/:id` โ€” Get +- `POST /api/articles` โ€” Create +- `PUT /api/articles/:id` โ€” Update +- `DELETE /api/articles/:id` โ€” Delete + +#### tags +- `GET /api/tags` โ€” List +- `GET /api/tags/:id` โ€” Get +- `POST /api/tags` โ€” Create +- `PUT /api/tags/:id` โ€” Update +- `DELETE /api/tags/:id` โ€” Delete + +#### comments +- `GET /api/comments` โ€” List +- `GET /api/comments/:id` โ€” Get +- `POST /api/comments` โ€” Create +- `PUT /api/comments/:id` โ€” Update +- `DELETE /api/comments/:id` โ€” Delete + +#### media +- `GET /api/media` โ€” List +- `GET /api/media/:id` โ€” Get +- `POST /api/media` โ€” Create +- `PUT /api/media/:id` โ€” Update +- `DELETE /api/media/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/blog-cms/fullstack/apps/api/.gitignore b/.benchmark/blog-cms/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/blog-cms/fullstack/apps/api/README.md b/.benchmark/blog-cms/fullstack/apps/api/README.md new file mode 100644 index 0000000..945cc2f --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/README.md @@ -0,0 +1,118 @@ +# SocialApp โ€” Backend API + +> ไธ€ๆฌพไปฅๅ†…ๅฎนๅˆ†ไบซๅ’Œ็”จๆˆทไบ’ๅŠจไธบๆ ธๅฟƒ็š„็คพไบคๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### articles +- `GET /api/articles` โ€” List all +- `GET /api/articles/:id` โ€” Get by ID +- `POST /api/articles` โ€” Create +- `PUT /api/articles/:id` โ€” Update +- `DELETE /api/articles/:id` โ€” Delete + +#### tags +- `GET /api/tags` โ€” List all +- `GET /api/tags/:id` โ€” Get by ID +- `POST /api/tags` โ€” Create +- `PUT /api/tags/:id` โ€” Update +- `DELETE /api/tags/:id` โ€” Delete + +#### comments +- `GET /api/comments` โ€” List all +- `GET /api/comments/:id` โ€” Get by ID +- `POST /api/comments` โ€” Create +- `PUT /api/comments/:id` โ€” Update +- `DELETE /api/comments/:id` โ€” Delete + +#### media +- `GET /api/media` โ€” List all +- `GET /api/media/:id` โ€” Get by ID +- `POST /api/media` โ€” Create +- `PUT /api/media/:id` โ€” Update +- `DELETE /api/media/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/blog-cms/fullstack/apps/api/package.json b/.benchmark/blog-cms/fullstack/apps/api/package.json new file mode 100644 index 0000000..7d7e801 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "socialapp-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts new file mode 100644 index 0000000..7136843 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/items.test.ts @@ -0,0 +1,118 @@ +// Item CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/items", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/items", () => { + it("creates a item", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/items", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/items/:id", () => { + it("returns the created item", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/items/:id", () => { + it("updates the item", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "data": "sample-data" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/items/:id", () => { + it("deletes the item", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/items/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/db/client.ts b/.benchmark/blog-cms/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts b/.benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..c1e9b27 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,20 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS articles (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n content TEXT,\n excerpt TEXT,\n cover_url TEXT,\n status TEXT,\n author_id TEXT REFERENCES authors(id),\n category TEXT,\n published_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS tags (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL UNIQUE,\n color TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS comments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n content TEXT NOT NULL,\n parent_id TEXT REFERENCES parents(id),\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS media (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n filename TEXT NOT NULL,\n url TEXT NOT NULL,\n mime_type TEXT,\n size_bytes INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/index.ts b/.benchmark/blog-cms/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..3592aea --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/index.ts @@ -0,0 +1,79 @@ +// SocialApp โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { articlesRoutes } from "./routes/articles.js"; +import { tagsRoutes } from "./routes/tags.js"; +import { commentsRoutes } from "./routes/comments.js"; +import { mediaRoutes } from "./routes/media.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-8106e913"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(articlesRoutes, { prefix: "/api/articles" }); + await app.register(tagsRoutes, { prefix: "/api/tags" }); + await app.register(commentsRoutes, { prefix: "/api/comments" }); + await app.register(mediaRoutes, { prefix: "/api/media" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts b/.benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts b/.benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts new file mode 100644 index 0000000..2ad945e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/routes/items.ts @@ -0,0 +1,51 @@ +// Auto-generated Item routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ItemService } from "../services/item.js"; +import type { CreateItemInput, UpdateItemInput } from "../types/index.js"; + +const service = new ItemService(); + +export async function itemsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/items โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/items/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/items โ€” create + app.post<{ Body: CreateItemInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/items/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/items/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts b/.benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/services/item.ts b/.benchmark/blog-cms/fullstack/apps/api/src/services/item.ts new file mode 100644 index 0000000..24ec745 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/services/item.ts @@ -0,0 +1,55 @@ +// Auto-generated Item service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js"; + +export class ItemService { + /** List all items */ + list(): Item[] { + return queryAll("SELECT * FROM items ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Item | undefined { + return queryOne("SELECT * FROM items WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateItemInput): Item { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "title", "data", "created_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.data ?? null, now]; + + execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateItemInput): Item | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM items WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/services/user.ts b/.benchmark/blog-cms/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/types/index.ts b/.benchmark/blog-cms/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..9b200cd --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,223 @@ +// Import and re-export shared entity types +import type { + User, + Contract, + Approval, + Customer, + Reminder, + Articles, + Tags, + Comments, + Media, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Contract, + Approval, + Customer, + Reminder, + Articles, + Tags, + Comments, + Media, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateArticlesInput { + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface CreateTagsInput { + userId?: string; + name: string; + color?: string; +} + +export interface CreateCommentsInput { + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; +} + +export interface CreateMediaInput { + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateArticlesInput { + userId?: string; + title?: string; + content?: string; + excerpt?: string; + coverUrl?: string; + authorId?: string; + category?: string; + publishedAt?: string; +} + +export interface UpdateTagsInput { + userId?: string; + name?: string; + color?: string; +} + +export interface UpdateCommentsInput { + userId?: string; + entityType?: string; + entityId?: string; + content?: string; + parentId?: string; +} + +export interface UpdateMediaInput { + userId?: string; + filename?: string; + url?: string; + mimeType?: string; + sizeBytes?: number; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/blog-cms/fullstack/apps/api/tsconfig.json b/.benchmark/blog-cms/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/globals.css b/.benchmark/blog-cms/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/layout.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..3613994 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "SocialApp", + description: "ไธ€ๆฌพไปฅๅ†…ๅฎนๅˆ†ไบซๅ’Œ็”จๆˆทไบ’ๅŠจไธบๆ ธๅฟƒ็š„็คพไบคๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx new file mode 100644 index 0000000..bd9a20e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/messages/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆถˆๆฏ

+

็งไฟกๅˆ—่กจใ€่Šๅคฉ

+
+ +
+ + { +

ๆถˆๆฏ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /messages

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..eb4a2cc --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

SocialApp

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ๆ–‡็ซ ๅ‘ๅธƒ + + + ๐Ÿ“… + ๅˆ†็ฑปๆ ‡็ญพ + + + ๐Ÿ“ + ่ฏ„่ฎบ็ฎก็† + + + ๐Ÿ“ธ + ๅช’ไฝ“ๅบ“ + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ SocialApp

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx new file mode 100644 index 0000000..3933192 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/profile/[userId]/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ไธชไบบ

+

ไธชไบบไธป้กต

+
+ +
+ + { +

ไธชไบบ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile/:userId

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx b/.benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx new file mode 100644 index 0000000..e09151b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/app/publish/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅ‘ๅธƒ

+

็ผ–่พ‘ๅ™จใ€็›ธๅ†Œ้€‰ๆ‹ฉ

+
+ +
+ + { +

ๅ‘ๅธƒ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /publish

+
} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts new file mode 100644 index 0000000..b6bfd57 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useComments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for comments +type Comment = Record; + +/** + * Fetch and manage comments data. + */ +export function useComments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/comments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts new file mode 100644 index 0000000..18364d8 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useFeed.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for feed +type Feed = Record; + +/** + * Fetch and manage feed data. + */ +export function useFeed() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/feed`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts new file mode 100644 index 0000000..6fa9aae --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/usePosts.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for posts +type Post = Record; + +/** + * Fetch and manage posts data. + */ +export function usePosts() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/posts`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts b/.benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts new file mode 100644 index 0000000..9586fc9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/hooks/useUsers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for users +type User = Record; + +/** + * Fetch and manage users data. + */ +export function useUsers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/users`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/next.config.ts b/.benchmark/blog-cms/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/blog-cms/fullstack/apps/web/package.json b/.benchmark/blog-cms/fullstack/apps/web/package.json new file mode 100644 index 0000000..13d054b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "socialapp-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/web/postcss.config.js b/.benchmark/blog-cms/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/api.ts b/.benchmark/blog-cms/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..cac42f9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for SocialApp + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/comments.ts b/.benchmark/blog-cms/fullstack/apps/web/services/comments.ts new file mode 100644 index 0000000..7df69f4 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/comments.ts @@ -0,0 +1,10 @@ +// Auto-generated comments service +import { api } from "./api"; + +export function get() { + return api.get("/api/comments"); +} + +export function post(data: Record) { + return api.post("/api/comments", data); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/feed.ts b/.benchmark/blog-cms/fullstack/apps/web/services/feed.ts new file mode 100644 index 0000000..85b119b --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/feed.ts @@ -0,0 +1,6 @@ +// Auto-generated feed service +import { api } from "./api"; + +export function get() { + return api.get("/api/feed"); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/posts.ts b/.benchmark/blog-cms/fullstack/apps/web/services/posts.ts new file mode 100644 index 0000000..6aeb9b7 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/posts.ts @@ -0,0 +1,10 @@ +// Auto-generated posts service +import { api } from "./api"; + +export function post(data: Record) { + return api.post("/api/posts", data); +} + +export function post_id_like(data: Record) { + return api.post("/api/posts", data); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/services/users.ts b/.benchmark/blog-cms/fullstack/apps/web/services/users.ts new file mode 100644 index 0000000..4b573b4 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/services/users.ts @@ -0,0 +1,6 @@ +// Auto-generated users service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/users/${id}`); +} diff --git a/.benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts b/.benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/blog-cms/fullstack/apps/web/tsconfig.json b/.benchmark/blog-cms/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/apps/web/types/index.ts b/.benchmark/blog-cms/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..0f93fab --- /dev/null +++ b/.benchmark/blog-cms/fullstack/apps/web/types/index.ts @@ -0,0 +1,130 @@ +// Auto-generated types for SocialApp + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/package.json b/.benchmark/blog-cms/fullstack/package.json new file mode 100644 index 0000000..25dab6e --- /dev/null +++ b/.benchmark/blog-cms/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-config/package.json b/.benchmark/blog-cms/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts b/.benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL โ€” reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json b/.benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-types/package.json b/.benchmark/blog-cms/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts b/.benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..64821a6 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,132 @@ +// Shared types for fullstack project +// Auto-generated โ€” used by both apps/web and apps/api + +// โ”€โ”€โ”€ Entities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Articles { + id: string; + userId?: string; + title: string; + content?: string; + excerpt?: string; + coverUrl?: string; + status?: string; + authorId?: string; + category?: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Tags { + id: string; + userId?: string; + name: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Comments { + id: string; + userId?: string; + entityType: string; + entityId: string; + content: string; + parentId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Media { + id: string; + userId?: string; + filename: string; + url: string; + mimeType?: string; + sizeBytes?: number; + createdAt?: string; + updatedAt?: string; +} + +// โ”€โ”€โ”€ API Response Wrappers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json b/.benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/fullstack/scripts/build.mjs b/.benchmark/blog-cms/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script โ€” builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n๐Ÿ”จ ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("๐Ÿ—๏ธ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\nโœ… Build complete!"); +} catch (e) { + console.error("\nโŒ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/blog-cms/fullstack/scripts/dev.mjs b/.benchmark/blog-cms/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/blog-cms/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script โ€” starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("๐Ÿš€ Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/blog-cms/prd.json b/.benchmark/blog-cms/prd.json new file mode 100644 index 0000000..58018ba --- /dev/null +++ b/.benchmark/blog-cms/prd.json @@ -0,0 +1 @@ +{"projectName":"SocialApp","chineseName":"็คพไบคๅนณๅฐ","domain":"social","matchConfidence":"high","summary":"ไธ€ๆฌพไปฅๅ†…ๅฎนๅˆ†ไบซๅ’Œ็”จๆˆทไบ’ๅŠจไธบๆ ธๅฟƒ็š„็คพไบคๅบ”็”จใ€‚","personas":[{"name":"ๅ†…ๅฎนๅˆ›ไฝœ่€…","description":"ๅ‘ๅธƒๅ›พๆ–‡/่ง†้ข‘ๅ†…ๅฎน็š„ๆดป่ทƒ็”จๆˆท","painPoints":["ๅ†…ๅฎนๆ›ๅ…‰ไธ่ถณ","ไบ’ๅŠจ็ฎก็†ๅคๆ‚"]},{"name":"ๆ™ฎ้€š็”จๆˆท","description":"ๆต่งˆๅ†…ๅฎนๅ’Œไบ’ๅŠจ็š„็”จๆˆท","painPoints":["ไฟกๆฏ่ฟ‡่ฝฝ","้š็ง้กพ่™‘"]}],"userStories":[{"id":"US-001","as":"ๅ†…ๅฎนๅˆ›ไฝœ่€…","want":"ๆ–‡็ซ ๅ‘ๅธƒ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅ†…ๅฎนๆ›ๅ…‰ไธ่ถณ","priority":"P0"},{"id":"US-002","as":"ๅ†…ๅฎนๅˆ›ไฝœ่€…","want":"ๅˆ†็ฑปๆ ‡็ญพ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅ†…ๅฎนๆ›ๅ…‰ไธ่ถณ","priority":"P0"},{"id":"US-003","as":"ๅ†…ๅฎนๅˆ›ไฝœ่€…","want":"่ฏ„่ฎบ็ฎก็†","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅ†…ๅฎนๆ›ๅ…‰ไธ่ถณ","priority":"P0"},{"id":"US-004","as":"ๆ™ฎ้€š็”จๆˆท","want":"ๆ–‡็ซ ๅ‘ๅธƒ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšไฟกๆฏ่ฟ‡่ฝฝ","priority":"P0"},{"id":"US-005","as":"ๆ™ฎ้€š็”จๆˆท","want":"ๅˆ†็ฑปๆ ‡็ญพ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšไฟกๆฏ่ฟ‡่ฝฝ","priority":"P0"},{"id":"US-006","as":"ๆ™ฎ้€š็”จๆˆท","want":"่ฏ„่ฎบ็ฎก็†","soThat":"่งฃๅ†ณ็—›็‚น๏ผšไฟกๆฏ่ฟ‡่ฝฝ","priority":"P0"}],"mvpScope":{"description":"MVP ่š็„ฆ ๆ–‡็ซ ๅ‘ๅธƒใ€ๅˆ†็ฑปๆ ‡็ญพใ€่ฏ„่ฎบ็ฎก็†ใ€ๅช’ไฝ“ๅบ“๏ผŒๅŒ…ๅซ้กต้ข๏ผš้ฆ–้กตใ€ๆ–‡็ซ ๅ‘ๅธƒใ€ๅˆ†็ฑปๆ ‡็ญพ","features":["ๆ–‡็ซ ๅ‘ๅธƒ","ๅˆ†็ฑปๆ ‡็ญพ","่ฏ„่ฎบ็ฎก็†","ๅช’ไฝ“ๅบ“"],"pages":["้ฆ–้กต","ๆ–‡็ซ ๅ‘ๅธƒ","ๅˆ†็ฑปๆ ‡็ญพ"],"estimatedWeeks":3},"features":[{"name":"ๆ–‡็ซ ๅ‘ๅธƒ","description":"ๆ–‡็ซ ๅ‘ๅธƒ","priority":"P0"},{"name":"ๅˆ†็ฑปๆ ‡็ญพ","description":"ๅˆ†็ฑปๆ ‡็ญพ","priority":"P0"},{"name":"่ฏ„่ฎบ็ฎก็†","description":"่ฏ„่ฎบ็ฎก็†","priority":"P0"},{"name":"ๅช’ไฝ“ๅบ“","description":"ๅช’ไฝ“ๅบ“","priority":"P0"}],"pages":[{"name":"้ฆ–้กต","route":"/home","description":"ๅบ”็”จ้ฆ–้กต"},{"name":"ๆ–‡็ซ ๅ‘ๅธƒ","route":"/articles","description":"ๆ–‡็ซ ๅ‘ๅธƒ"},{"name":"ๅˆ†็ฑปๆ ‡็ญพ","route":"/tags","description":"ๅˆ†็ฑปๆ ‡็ญพ"},{"name":"่ฏ„่ฎบ็ฎก็†","route":"/comments","description":"่ฏ„่ฎบ็ฎก็†"},{"name":"ๅช’ไฝ“ๅบ“","route":"/media","description":"ๅช’ไฝ“ๅบ“"}],"apiRequirements":[{"method":"GET","path":"/api/articles","description":"่Žทๅ–ๆ–‡็ซ ๅ‘ๅธƒๅˆ—่กจ"},{"method":"POST","path":"/api/articles","description":"ๅˆ›ๅปบๆ–‡็ซ ๅ‘ๅธƒ"},{"method":"GET","path":"/api/tags","description":"่Žทๅ–ๅˆ†็ฑปๆ ‡็ญพๅˆ—่กจ"},{"method":"POST","path":"/api/tags","description":"ๅˆ›ๅปบๅˆ†็ฑปๆ ‡็ญพ"},{"method":"GET","path":"/api/comments","description":"่Žทๅ–่ฏ„่ฎบ็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/comments","description":"ๅˆ›ๅปบ่ฏ„่ฎบ็ฎก็†"},{"method":"GET","path":"/api/media","description":"่Žทๅ–ๅช’ไฝ“ๅบ“ๅˆ—่กจ"},{"method":"POST","path":"/api/media","description":"ๅˆ›ๅปบๅช’ไฝ“ๅบ“"},{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter๏ผˆ่ทจๅนณๅฐ๏ผ‰","considerations":["ๅปบ่ฎฎ่ทจๅนณๅฐๆก†ๆžถๅ‡ๅฐ‘ๅผ€ๅ‘ๆˆๆœฌ"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"้กน็›ฎ่„šๆ‰‹ๆžถๆญๅปบ","description":"ๅˆๅง‹ๅŒ– SocialApp ้กน็›ฎ็ป“ๆž„๏ผŒ้…็ฝฎๆž„ๅปบๅทฅๅ…ทๅ’Œ CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"ๆ•ฐๆฎๅบ“่ฎพ่ฎกไธŽๅˆๅง‹ๅŒ–","description":"่ฎพ่ฎกๆ•ฐๆฎๆจกๅž‹๏ผŒๅˆ›ๅปบๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌ","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"้กต้ขๅผ€ๅ‘๏ผš้ฆ–้กต","description":"ๅผ€ๅ‘ ้ฆ–้กต ้กต้ข๏ผˆ/home๏ผ‰๏ผšๅบ”็”จ้ฆ–้กต","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"้กต้ขๅผ€ๅ‘๏ผšๆ–‡็ซ ๅ‘ๅธƒ","description":"ๅผ€ๅ‘ ๆ–‡็ซ ๅ‘ๅธƒ ้กต้ข๏ผˆ/articles๏ผ‰๏ผšๆ–‡็ซ ๅ‘ๅธƒ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"้กต้ขๅผ€ๅ‘๏ผšๅˆ†็ฑปๆ ‡็ญพ","description":"ๅผ€ๅ‘ ๅˆ†็ฑปๆ ‡็ญพ ้กต้ข๏ผˆ/tags๏ผ‰๏ผšๅˆ†็ฑปๆ ‡็ญพ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"้กต้ขๅผ€ๅ‘๏ผš่ฏ„่ฎบ็ฎก็†","description":"ๅผ€ๅ‘ ่ฏ„่ฎบ็ฎก็† ้กต้ข๏ผˆ/comments๏ผ‰๏ผš่ฏ„่ฎบ็ฎก็†","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API ๅผ€ๅ‘๏ผšGET /api/articles","description":"่Žทๅ–ๆ–‡็ซ ๅ‘ๅธƒๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API ๅผ€ๅ‘๏ผšPOST /api/articles","description":"ๅˆ›ๅปบๆ–‡็ซ ๅ‘ๅธƒ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API ๅผ€ๅ‘๏ผšGET /api/tags","description":"่Žทๅ–ๅˆ†็ฑปๆ ‡็ญพๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API ๅผ€ๅ‘๏ผšPOST /api/tags","description":"ๅˆ›ๅปบๅˆ†็ฑปๆ ‡็ญพ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"ๅ‰ๅŽ็ซฏ่”่ฐƒ","description":"ๅ‰็ซฏ้กต้ขไธŽๅŽ็ซฏ API ้›†ๆˆ่”่ฐƒ","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"ๆต‹่ฏ•ไธŽไฟฎๅค","description":"ๅŠŸ่ƒฝๆต‹่ฏ•ใ€Bug ไฟฎๅคใ€ๆ€ง่ƒฝไผ˜ๅŒ–","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:35.532Z","inputLength":32,"domainMatchCount":1,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/README.md b/.benchmark/blog-cms/release/release/README.md new file mode 100644 index 0000000..eae602e --- /dev/null +++ b/.benchmark/blog-cms/release/release/README.md @@ -0,0 +1,33 @@ +# socialapp โ€” Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:36.006Z + +## Directory Structure + +``` +release/ +โ”œโ”€โ”€ version.json โ€” Version metadata +โ”œโ”€โ”€ build-info.json โ€” Build environment info +โ”œโ”€โ”€ README.md โ€” This file +โ”œโ”€โ”€ manifests/ +โ”‚ โ””โ”€โ”€ manifest.json โ€” Release manifest with file hashes +โ”œโ”€โ”€ checksums/ +โ”‚ โ””โ”€โ”€ checksums.txt โ€” SHA256 checksums for verification +โ”œโ”€โ”€ release-notes/ +โ”‚ โ””โ”€โ”€ release-notes.md โ€” Human-readable release notes +โ”œโ”€โ”€ windows/ โ€” Windows installers +โ”œโ”€โ”€ macos/ โ€” macOS disk images +โ””โ”€โ”€ linux/ โ€” Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/build-info.json b/.benchmark/blog-cms/release/release/build-info.json new file mode 100644 index 0000000..d84d88b --- /dev/null +++ b/.benchmark/blog-cms/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:36.006Z", + "domain": "unknown", + "entityCount": 11, + "routeCount": 1, + "screenCount": 11, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "socialapp", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/checksums/checksums.txt b/.benchmark/blog-cms/release/release/checksums/checksums.txt new file mode 100644 index 0000000..88424d1 --- /dev/null +++ b/.benchmark/blog-cms/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +bb8b8b420d0aa06a4fcede9291db577eebfa1b80def8d70bbbed6c4de79519d0 windows/socialapp-setup-0.1.0.exe +ad0a1855753c7e0fffae07f89042dc7fea92d00d03af3963392d3d975d36e18a windows/socialapp-0.1.0-portable.exe +7d5fb02ede1f0c7c74f5e5b78ddd2096f186cfc5f16421eb8a861739118a8854 macos/socialapp-0.1.0.dmg +d687104970423e01f05af053c12747b5d1f79da5d7ebfbd2a5f924c3aef7bb1a macos/socialapp-0.1.0-arm64.dmg +92ce5ef3f6b3856ac36999ebaff3fe53a0e20ec2fe4d5dc8c47c3562019e1484 linux/socialapp-0.1.0.AppImage +279fd2bc685d8f8d60e556d72f2bd59445c10ee793835297b76279f89cd2ca5a linux/socialapp_0.1.0_amd64.deb diff --git a/.benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage b/.benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage new file mode 100644 index 0000000..7223e9b --- /dev/null +++ b/.benchmark/blog-cms/release/release/linux/socialapp-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb b/.benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb new file mode 100644 index 0000000..6ec89b5 --- /dev/null +++ b/.benchmark/blog-cms/release/release/linux/socialapp_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg new file mode 100644 index 0000000..208f89d --- /dev/null +++ b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg new file mode 100644 index 0000000..b24c61c --- /dev/null +++ b/.benchmark/blog-cms/release/release/macos/socialapp-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/manifests/manifest.json b/.benchmark/blog-cms/release/release/manifests/manifest.json new file mode 100644 index 0000000..4c7e6cd --- /dev/null +++ b/.benchmark/blog-cms/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "socialapp", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:36.006Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/socialapp-setup-0.1.0.exe", + "size": 144, + "sha256": "bb8b8b420d0aa06a4fcede9291db577eebfa1b80def8d70bbbed6c4de79519d0" + }, + { + "path": "windows/socialapp-0.1.0-portable.exe", + "size": 148, + "sha256": "ad0a1855753c7e0fffae07f89042dc7fea92d00d03af3963392d3d975d36e18a" + }, + { + "path": "macos/socialapp-0.1.0.dmg", + "size": 140, + "sha256": "7d5fb02ede1f0c7c74f5e5b78ddd2096f186cfc5f16421eb8a861739118a8854" + }, + { + "path": "macos/socialapp-0.1.0-arm64.dmg", + "size": 142, + "sha256": "d687104970423e01f05af053c12747b5d1f79da5d7ebfbd2a5f924c3aef7bb1a" + }, + { + "path": "linux/socialapp-0.1.0.AppImage", + "size": 133, + "sha256": "92ce5ef3f6b3856ac36999ebaff3fe53a0e20ec2fe4d5dc8c47c3562019e1484" + }, + { + "path": "linux/socialapp_0.1.0_amd64.deb", + "size": 143, + "sha256": "279fd2bc685d8f8d60e556d72f2bd59445c10ee793835297b76279f89cd2ca5a" + } + ], + "totalFiles": 6, + "totalSize": 850 +} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/release-notes/release-notes.md b/.benchmark/blog-cms/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..f05d6bd --- /dev/null +++ b/.benchmark/blog-cms/release/release/release-notes/release-notes.md @@ -0,0 +1,60 @@ +# socialapp v0.1.0 + +> Release Builder Agent โ€” SF-07 +> Generated: 2026-06-05 22:08:36 UTC + +## Version + +- **Name:** socialapp +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- approvals +- articles +- auth +- comments +- contracts +- customers +- items +- media +- reminders +- tags +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:36.006Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- โœ… windows +- โœ… macos +- โœ… linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/version.json b/.benchmark/blog-cms/release/release/version.json new file mode 100644 index 0000000..00ba6ca --- /dev/null +++ b/.benchmark/blog-cms/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "socialapp", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe b/.benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe new file mode 100644 index 0000000..ca6f54a --- /dev/null +++ b/.benchmark/blog-cms/release/release/windows/socialapp-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:36.005Z diff --git a/.benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe b/.benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe new file mode 100644 index 0000000..d087963 --- /dev/null +++ b/.benchmark/blog-cms/release/release/windows/socialapp-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] socialapp v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:36.001Z diff --git a/.benchmark/contract-mgmt/arch.json b/.benchmark/contract-mgmt/arch.json new file mode 100644 index 0000000..2782f82 --- /dev/null +++ b/.benchmark/contract-mgmt/arch.json @@ -0,0 +1 @@ +{"projectName":"OAFlow","domain":"enterprise","contract":{"projectName":"OAFlow","domain":"enterprise","entities":[{"name":"User","table":"users","description":"็ณป็ปŸ็”จๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"hasMany","entity":"contracts","via":"user_id"},{"type":"hasMany","entity":"customers","via":"user_id"}]},{"name":"Contract","table":"contracts","description":"ๅˆๅŒ","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","defaultValue":"draft","enum":["draft","pending","active","expired","terminated"]},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Approval","table":"approvals","description":"ๅฎกๆ‰นๆต็จ‹","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"applicant_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"status","type":"VARCHAR(32)","defaultValue":"pending","enum":["pending","approved","rejected"]},{"name":"form_data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"users","via":"applicant_id","as":"applicant"}]},{"name":"Customer","table":"customers","description":"ๅฎขๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","defaultValue":"[]"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Reminder","table":"reminders","description":"ๅˆฐๆœŸๆ้†’","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":true,"fkEntity":"users","fkColumn":"id"},{"name":"entity_type","type":"VARCHAR(32)","required":true},{"name":"entity_id","type":"UUID"},{"name":"remind_at","type":"TIMESTAMP","required":true},{"name":"message","type":"TEXT"},{"name":"sent","type":"BOOLEAN","defaultValue":"false"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Stats","table":"stats","description":"ๅˆๅŒ้‡‘้ข็ปŸ่ฎก่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"metric","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false},{"name":"value","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"period","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":false},{"name":"recorded_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"OperationLogs","table":"operation_logs","description":"ๆ“ไฝœๆ—ฅๅฟ—่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"status","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":true},{"name":"data","type":"JSONB","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":8,"requireSpecial":true}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","remind_at":"remindAt","recorded_at":"recordedAt","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","remindAt":"remind_at","recordedAt":"recorded_at","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Contract":{"user_id":{"required":true},"title":{"required":true},"status":{"enum":["draft","pending","active","expired","terminated"],"defaultValue":"draft"}},"Approval":{"user_id":{"required":true},"entity_type":{"required":true},"applicant_id":{"required":true},"status":{"enum":["pending","approved","rejected"],"defaultValue":"pending"}},"Customer":{"user_id":{"required":true},"name":{"required":true},"tags":{"defaultValue":"[]"}},"Reminder":{"user_id":{"required":true},"entity_type":{"required":true},"remind_at":{"required":true},"sent":{"defaultValue":"false"}},"Stats":{"id":{"required":true},"metric":{"required":true}},"OperationLogs":{"id":{"required":true},"title":{"required":true}}}},"techStack":{"frontend":"React 19 + TypeScript + Vite","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7","deployment":"Docker + Nginx","considerations":[]},"architectureDiagram":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ OAFlow โ€” System Architecture โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Client Layer โ”‚ โ”‚ Admin / Web โ”‚ โ”‚\nโ”‚ โ”‚ React 19 + TypeScript + Vite โ”‚ โ”‚ React + Vite (Web) โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ API Gateway โ”‚ โ”‚\nโ”‚ โ”‚ Nginx / Kong โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Backend Services โ”‚ โ”‚\nโ”‚ โ”‚ NestJS + Prisma โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ PostgreSQL 15โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ MinIO โ”‚ โ”‚\nโ”‚ โ”‚ Primary โ”‚ โ”‚ Session โ”‚ โ”‚ Files โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ Modules: โ”‚\nโ”‚ 1 ๅˆๅŒๅˆ›ๅปบ โ”‚\nโ”‚ 2 Workflow โ”‚\nโ”‚ 3 Notification โ”‚\nโ”‚ 4 ๅฎขๆˆท็ฎก็† โ”‚\nโ”‚ 5 Analytics โ”‚\nโ”‚ 6 ๆ“ไฝœๆ—ฅๅฟ— โ”‚\nโ”‚ โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","dataFlows":[{"page":"ๅˆๅŒๅˆ›ๅปบ","route":"/contracts","description":"ๅˆๅŒๅˆ›ๅปบ","dataFlow":["GET /api/contracts โ†’ ่Žทๅ–ๅˆๅŒๅˆ›ๅปบๅˆ—่กจ","POST /api/contracts โ†’ ๅˆ›ๅปบๅˆๅŒๅˆ›ๅปบ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅˆๅŒๅฎกๆ‰น","route":"/approvals","description":"ๅˆๅŒๅฎกๆ‰น","dataFlow":["GET /api/approvals โ†’ ่Žทๅ–ๅˆๅŒๅฎกๆ‰นๅˆ—่กจ","POST /api/approvals โ†’ ๅˆ›ๅปบๅˆๅŒๅฎกๆ‰น"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅˆๅŒๅˆฐๆœŸๆ้†’","route":"/reminders","description":"ๅˆๅŒๅˆฐๆœŸๆ้†’","dataFlow":["GET /api/reminders โ†’ ่Žทๅ–ๅˆๅŒๅˆฐๆœŸๆ้†’ๅˆ—่กจ","POST /api/reminders โ†’ ๅˆ›ๅปบๅˆๅŒๅˆฐๆœŸๆ้†’"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅฎขๆˆท็ฎก็†","route":"/customers","description":"ๅฎขๆˆท็ฎก็†","dataFlow":["GET /api/customers โ†’ ่Žทๅ–ๅฎขๆˆท็ฎก็†ๅˆ—่กจ","POST /api/customers โ†’ ๅˆ›ๅปบๅฎขๆˆท็ฎก็†"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"}],"modules":[{"name":"ๅˆๅŒๅˆ›ๅปบ","label":"ๅˆๅŒๅˆ›ๅปบ","features":["ๅˆๅŒๅˆ›ๅปบ"],"responsibilities":["ๆไพ› ๅˆๅŒๅˆ›ๅปบ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"workflow","label":"Workflow","features":["ๅˆๅŒๅฎกๆ‰น"],"responsibilities":["ๆไพ› ๅˆๅŒๅฎกๆ‰น ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"notification","label":"Notification","features":["ๅˆๅŒๅˆฐๆœŸๆ้†’"],"responsibilities":["ๆไพ› ๅˆๅŒๅˆฐๆœŸๆ้†’ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ๅฎขๆˆท็ฎก็†","label":"ๅฎขๆˆท็ฎก็†","features":["ๅฎขๆˆท็ฎก็†"],"responsibilities":["ๆไพ› ๅฎขๆˆท็ฎก็† ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"analytics","label":"Analytics","features":["ๅˆๅŒ้‡‘้ข็ปŸ่ฎก"],"responsibilities":["ๆไพ› ๅˆๅŒ้‡‘้ข็ปŸ่ฎก ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ๆ“ไฝœๆ—ฅๅฟ—","label":"ๆ“ไฝœๆ—ฅๅฟ—","features":["ๆ“ไฝœๆ—ฅๅฟ—"],"responsibilities":["ๆไพ› ๆ“ไฝœๆ—ฅๅฟ— ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"approval","label":"ๅฎกๆ‰นๅผ•ๆ“Ž","features":["ๅฎกๆ‰นๆต็จ‹"],"responsibilities":["ๆไพ› ๅฎกๆ‰นๆต็จ‹ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"attendance","label":"่€ƒๅ‹ค็ฎก็†","features":["่€ƒๅ‹คๆ‰“ๅก"],"responsibilities":["ๆไพ› ่€ƒๅ‹คๆ‰“ๅก ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"department","label":"็ป„็ป‡ๆžถๆž„","features":["้ƒจ้—จ็ฎก็†"],"responsibilities":["ๆไพ› ้ƒจ้—จ็ฎก็† ็›ธๅ…ณๅŠŸ่ƒฝ"]}],"databaseSchema":[{"table":"users","description":"็”จๆˆท่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"contracts","description":"ๅˆๅŒๅˆ›ๅปบ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_contracts_user ON contracts(user_id)"]},{"table":"approvals","description":"ๅˆๅŒๅฎกๆ‰น่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_approvals_user ON approvals(user_id)"]},{"table":"reminders","description":"ๅˆๅŒๅˆฐๆœŸๆ้†’่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"party_a","type":"VARCHAR(128)"},{"name":"party_b","type":"VARCHAR(128)"},{"name":"amount","type":"DECIMAL(14,2)"},{"name":"signed_at","type":"DATE"},{"name":"expires_at","type":"DATE"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'draft'"},{"name":"file_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_reminders_user ON reminders(user_id)"]},{"table":"customers","description":"ๅฎขๆˆท็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","constraints":"DEFAULT '[]'"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_customers_user ON customers(user_id)"]},{"table":"stats","description":"ๅˆๅŒ้‡‘้ข็ปŸ่ฎก่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"metric","type":"VARCHAR(64)","constraints":"NOT NULL"},{"name":"value","type":"DECIMAL(12,2)"},{"name":"period","type":"VARCHAR(32)"},{"name":"recorded_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_stats_user ON stats(user_id)"]},{"table":"operation_logs","description":"ๆ“ไฝœๆ—ฅๅฟ—่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"status","type":"VARCHAR(32)","constraints":"DEFAULT 'active'"},{"name":"data","type":"JSONB"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_operation_logs_user ON operation_logs(user_id)"]}],"apiDesign":[{"resource":"approvals","basePath":"/api/approvals","endpoints":[{"method":"GET","path":"/api/approvals","description":"่Žทๅ–ๅˆๅŒๅฎกๆ‰นๅˆ—่กจ"},{"method":"POST","path":"/api/approvals","description":"ๅˆ›ๅปบๅˆๅŒๅฎกๆ‰น"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}]},{"resource":"contracts","basePath":"/api/contracts","endpoints":[{"method":"GET","path":"/api/contracts","description":"่Žทๅ–ๅˆๅŒๅˆ›ๅปบๅˆ—่กจ"},{"method":"POST","path":"/api/contracts","description":"ๅˆ›ๅปบๅˆๅŒๅˆ›ๅปบ"}]},{"resource":"customers","basePath":"/api/customers","endpoints":[{"method":"GET","path":"/api/customers","description":"่Žทๅ–ๅฎขๆˆท็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/customers","description":"ๅˆ›ๅปบๅฎขๆˆท็ฎก็†"}]},{"resource":"reminders","basePath":"/api/reminders","endpoints":[{"method":"GET","path":"/api/reminders","description":"่Žทๅ–ๅˆๅŒๅˆฐๆœŸๆ้†’ๅˆ—่กจ"},{"method":"POST","path":"/api/reminders","description":"ๅˆ›ๅปบๅˆๅŒๅˆฐๆœŸๆ้†’"}]}],"directoryStructure":["oaflow/","โ”œโ”€โ”€ apps/","โ”‚ โ”œโ”€โ”€ mobile/ # React Native / Flutter ็งปๅŠจ็ซฏ","โ”‚ โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # ้กต้ข็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆๅŒๅˆ›ๅปบ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ workflow/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ้€š็”จ็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # ่‡ชๅฎšไน‰ Hooks","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API ่ฐƒ็”จๅฑ‚","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ # ็Šถๆ€็ฎก็†","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ","โ”‚ โ”‚ โ”œโ”€โ”€ app.json","โ”‚ โ”‚ โ””โ”€โ”€ package.json","โ”‚ โ””โ”€โ”€ web/ # Web ็ฎก็†ๅŽๅฐ","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ pages/","โ”‚ โ”‚ โ”œโ”€โ”€ components/","โ”‚ โ”‚ โ””โ”€โ”€ layouts/","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ packages/","โ”‚ โ””โ”€โ”€ shared/ # ๅ…ฑไบซ็ฑปๅž‹/ๅธธ้‡","โ”œโ”€โ”€ server/ # NestJS ๅŽ็ซฏๆœๅŠก","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ modules/ # ไธšๅŠกๆจกๅ—","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆๅŒๅˆ›ๅปบ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆๅŒๅˆ›ๅปบ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆๅŒๅˆ›ๅปบ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅˆๅŒๅˆ›ๅปบ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ workflow/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ workflow.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ workflow.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ workflow.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ notification.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”œโ”€โ”€ common/ # ้€š็”จ๏ผˆguards, filters, interceptors๏ผ‰","โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ # Prisma ORM","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ schema.prisma","โ”‚ โ”‚ โ”œโ”€โ”€ config/ # ็Žฏๅขƒ้…็ฝฎ","โ”‚ โ”‚ โ””โ”€โ”€ main.ts","โ”‚ โ”œโ”€โ”€ test/","โ”‚ โ”œโ”€โ”€ Dockerfile","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ docker-compose.yml","โ”œโ”€โ”€ .github/workflows/ # CI/CD","โ”‚ โ””โ”€โ”€ ci.yml","โ”œโ”€โ”€ .env.example","โ”œโ”€โ”€ README.md","โ””โ”€โ”€ turbo.json # Monorepo ้…็ฝฎ"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker + Nginx","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions โ†’ Build โ†’ Test โ†’ Deploy"},"meta":{"generatedAt":"2026-06-05T13:43:26.925Z","sourceDomain":"enterprise","moduleCount":9,"tableCount":7,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/backend/.gitignore b/.benchmark/contract-mgmt/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/contract-mgmt/backend/README.md b/.benchmark/contract-mgmt/backend/README.md new file mode 100644 index 0000000..d0f7365 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/README.md @@ -0,0 +1,104 @@ +# OAFlow โ€” Backend API + +> ไธ€ๆฌพๆ”ฏๆŒๅฎกๆ‰นๆตใ€่€ƒๅ‹ค็ฎก็†ใ€้ƒจ้—จๅไฝœ็š„ไผไธšๅŠžๅ…ฌ่‡ชๅŠจๅŒ–็ณป็ปŸใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### contracts +- `GET /api/contracts` โ€” List all +- `GET /api/contracts/:id` โ€” Get by ID +- `POST /api/contracts` โ€” Create +- `PUT /api/contracts/:id` โ€” Update +- `DELETE /api/contracts/:id` โ€” Delete + +#### approvals +- `GET /api/approvals` โ€” List all +- `GET /api/approvals/:id` โ€” Get by ID +- `POST /api/approvals` โ€” Create +- `PUT /api/approvals/:id` โ€” Update +- `DELETE /api/approvals/:id` โ€” Delete + +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### reminders +- `GET /api/reminders` โ€” List all +- `GET /api/reminders/:id` โ€” Get by ID +- `POST /api/reminders` โ€” Create +- `PUT /api/reminders/:id` โ€” Update +- `DELETE /api/reminders/:id` โ€” Delete + +#### stats +- `GET /api/stats` โ€” List all +- `GET /api/stats/:id` โ€” Get by ID +- `POST /api/stats` โ€” Create +- `PUT /api/stats/:id` โ€” Update +- `DELETE /api/stats/:id` โ€” Delete + +#### operation_logs +- `GET /api/operation_logs` โ€” List all +- `GET /api/operation_logs/:id` โ€” Get by ID +- `POST /api/operation_logs` โ€” Create +- `PUT /api/operation_logs/:id` โ€” Update +- `DELETE /api/operation_logs/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/contract-mgmt/backend/package.json b/.benchmark/contract-mgmt/backend/package.json new file mode 100644 index 0000000..bd459f1 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "oaflow", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts new file mode 100644 index 0000000..392316d --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/approvals.test.ts @@ -0,0 +1,121 @@ +// Approval CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "applicantId": regRes.json().user.id, + "status": "sample-status", + "formData": "sample-formdata" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/approvals", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/approvals", () => { + it("creates a approval", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/approvals", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/approvals/:id", () => { + it("returns the created approval", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/approvals/:id", () => { + it("updates the approval", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/approvals/:id", () => { + it("deletes the approval", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/approvals/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts new file mode 100644 index 0000000..c2e9f16 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/contracts.test.ts @@ -0,0 +1,124 @@ +// Contract CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "partyA": "sample-partya", + "partyB": "sample-partyb", + "amount": 1, + "signedAt": "sample-signedat", + "expiresAt": "sample-expiresat", + "status": "sample-status", + "fileUrl": "sample-fileurl" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/contracts", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/contracts", () => { + it("creates a contract", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/contracts", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/contracts/:id", () => { + it("returns the created contract", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/contracts/:id", () => { + it("updates the contract", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/contracts/:id", () => { + it("deletes the contract", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/contracts/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..f3112fa --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/customers.test.ts @@ -0,0 +1,122 @@ +// Customer CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source", + "tags": "sample-tags" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts new file mode 100644 index 0000000..b90eff3 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/operation_logs.test.ts @@ -0,0 +1,119 @@ +// OperationLogs CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "data": "sample-data" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/operation_logs", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/operation_logs", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/operation_logs", () => { + it("creates a operation_log", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/operation_logs", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/operation_logs/:id", () => { + it("returns the created operation_log", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/operation_logs/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/operation_logs/:id", () => { + it("updates the operation_log", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/operation_logs/:id", () => { + it("deletes the operation_log", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/operation_logs/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts new file mode 100644 index 0000000..c573030 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/reminders.test.ts @@ -0,0 +1,121 @@ +// Reminder CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "entityType": "sample-entitytype", + "entityId": "sample-entityid", + "remindAt": "sample-remindat", + "message": "sample-message", + "sent": true + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/reminders", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/reminders", () => { + it("creates a reminder", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/reminders", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/reminders/:id", () => { + it("returns the created reminder", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/reminders/:id", () => { + it("updates the reminder", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/reminders/:id", () => { + it("deletes the reminder", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/reminders/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts b/.benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts new file mode 100644 index 0000000..1b64b6d --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/__tests__/stats.test.ts @@ -0,0 +1,119 @@ +// Stats CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "metric": "sample-metric", + "value": 1, + "period": "sample-period" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/stats", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/stats", () => { + it("creates a stat", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/stats", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/stats/:id", () => { + it("returns the created stat", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/stats/:id", () => { + it("updates the stat", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/stats/:id", () => { + it("deletes the stat", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/stats/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/contract-mgmt/backend/src/db/client.ts b/.benchmark/contract-mgmt/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/contract-mgmt/backend/src/db/schema.ts b/.benchmark/contract-mgmt/backend/src/db/schema.ts new file mode 100644 index 0000000..9b7b755 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/db/schema.ts @@ -0,0 +1,18 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS contracts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n title TEXT NOT NULL,\n party_a TEXT,\n party_b TEXT,\n amount REAL,\n signed_at TEXT,\n expires_at TEXT,\n status TEXT DEFAULT 'draft',\n file_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS approvals (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n applicant_id TEXT NOT NULL REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n form_data TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT DEFAULT '[]',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS reminders (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL REFERENCES users(id),\n entity_type TEXT NOT NULL,\n entity_id TEXT,\n remind_at TEXT NOT NULL,\n message TEXT,\n sent INTEGER DEFAULT 'false',\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n metric TEXT NOT NULL,\n value REAL,\n period TEXT,\n recorded_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS operation_logs (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/contract-mgmt/backend/src/index.ts b/.benchmark/contract-mgmt/backend/src/index.ts new file mode 100644 index 0000000..78b3edd --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/index.ts @@ -0,0 +1,75 @@ +// OAFlow โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { contractsRoutes } from "./routes/contracts.js"; +import { approvalsRoutes } from "./routes/approvals.js"; +import { customersRoutes } from "./routes/customers.js"; +import { remindersRoutes } from "./routes/reminders.js"; +import { statsRoutes } from "./routes/stats.js"; +import { operation_logsRoutes } from "./routes/operation_logs.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-145dc365"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(contractsRoutes, { prefix: "/api/contracts" }); + await app.register(approvalsRoutes, { prefix: "/api/approvals" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(remindersRoutes, { prefix: "/api/reminders" }); + await app.register(statsRoutes, { prefix: "/api/stats" }); + await app.register(operation_logsRoutes, { prefix: "/api/operation_logs" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/contract-mgmt/backend/src/middleware/auth.ts b/.benchmark/contract-mgmt/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/approvals.ts b/.benchmark/contract-mgmt/backend/src/routes/approvals.ts new file mode 100644 index 0000000..3a974d2 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/approvals.ts @@ -0,0 +1,51 @@ +// Auto-generated Approval routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ApprovalService } from "../services/approval.js"; +import type { CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +const service = new ApprovalService(); + +export async function approvalsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/approvals โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/approvals/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/approvals โ€” create + app.post<{ Body: CreateApprovalInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/approvals/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateApprovalInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/approvals/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Approval not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/auth.ts b/.benchmark/contract-mgmt/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/contracts.ts b/.benchmark/contract-mgmt/backend/src/routes/contracts.ts new file mode 100644 index 0000000..ee83abf --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/contracts.ts @@ -0,0 +1,51 @@ +// Auto-generated Contract routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ContractService } from "../services/contract.js"; +import type { CreateContractInput, UpdateContractInput } from "../types/index.js"; + +const service = new ContractService(); + +export async function contractsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/contracts โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/contracts/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/contracts โ€” create + app.post<{ Body: CreateContractInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/contracts/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateContractInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/contracts/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Contract not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/customers.ts b/.benchmark/contract-mgmt/backend/src/routes/customers.ts new file mode 100644 index 0000000..9a6f981 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customer routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomerService } from "../services/customer.js"; +import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +const service = new CustomerService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers โ€” create + app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/operation_logs.ts b/.benchmark/contract-mgmt/backend/src/routes/operation_logs.ts new file mode 100644 index 0000000..518a2b6 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/operation_logs.ts @@ -0,0 +1,51 @@ +// Auto-generated OperationLogs routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { OperationLogsService } from "../services/operation_log.js"; +import type { CreateOperationLogsInput, UpdateOperationLogsInput } from "../types/index.js"; + +const service = new OperationLogsService(); + +export async function operation_logsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/operation_logs โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/operation_logs/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "OperationLogs not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/operation_logs โ€” create + app.post<{ Body: CreateOperationLogsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/operation_logs/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateOperationLogsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "OperationLogs not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/operation_logs/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "OperationLogs not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/reminders.ts b/.benchmark/contract-mgmt/backend/src/routes/reminders.ts new file mode 100644 index 0000000..ee00754 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/reminders.ts @@ -0,0 +1,51 @@ +// Auto-generated Reminder routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ReminderService } from "../services/reminder.js"; +import type { CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +const service = new ReminderService(); + +export async function remindersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/reminders โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/reminders/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/reminders โ€” create + app.post<{ Body: CreateReminderInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/reminders/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateReminderInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/reminders/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Reminder not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/routes/stats.ts b/.benchmark/contract-mgmt/backend/src/routes/stats.ts new file mode 100644 index 0000000..92f64ea --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/routes/stats.ts @@ -0,0 +1,51 @@ +// Auto-generated Stats routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StatsService } from "../services/stat.js"; +import type { CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +const service = new StatsService(); + +export async function statsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/stats โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/stats/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/stats โ€” create + app.post<{ Body: CreateStatsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/stats/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateStatsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/stats/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Stats not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/contract-mgmt/backend/src/services/approval.ts b/.benchmark/contract-mgmt/backend/src/services/approval.ts new file mode 100644 index 0000000..f40b2a2 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/approval.ts @@ -0,0 +1,56 @@ +// Auto-generated Approval service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Approval, CreateApprovalInput, UpdateApprovalInput } from "../types/index.js"; + +export class ApprovalService { + /** List all approvals */ + list(): Approval[] { + return queryAll("SELECT * FROM approvals ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Approval | undefined { + return queryOne("SELECT * FROM approvals WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateApprovalInput): Approval { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "applicant_id", "status", "form_data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.applicantId ?? null, input.status ?? null, input.formData ?? null, now, now]; + + execute(`INSERT INTO approvals (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateApprovalInput): Approval | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.applicantId !== undefined) { sets.push("applicant_id = ?"); values.push(input.applicantId); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.formData !== undefined) { sets.push("form_data = ?"); values.push(input.formData); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE approvals SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM approvals WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/contract.ts b/.benchmark/contract-mgmt/backend/src/services/contract.ts new file mode 100644 index 0000000..7980109 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/contract.ts @@ -0,0 +1,59 @@ +// Auto-generated Contract service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Contract, CreateContractInput, UpdateContractInput } from "../types/index.js"; + +export class ContractService { + /** List all contracts */ + list(): Contract[] { + return queryAll("SELECT * FROM contracts ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Contract | undefined { + return queryOne("SELECT * FROM contracts WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateContractInput): Contract { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "party_a", "party_b", "amount", "signed_at", "expires_at", "status", "file_url", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.partyA ?? null, input.partyB ?? null, input.amount ?? null, input.signedAt ?? null, input.expiresAt ?? null, input.status ?? null, input.fileUrl ?? null, now, now]; + + execute(`INSERT INTO contracts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateContractInput): Contract | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.partyA !== undefined) { sets.push("party_a = ?"); values.push(input.partyA); } + if (input.partyB !== undefined) { sets.push("party_b = ?"); values.push(input.partyB); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.signedAt !== undefined) { sets.push("signed_at = ?"); values.push(input.signedAt); } + if (input.expiresAt !== undefined) { sets.push("expires_at = ?"); values.push(input.expiresAt); } + if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); } + if (input.fileUrl !== undefined) { sets.push("file_url = ?"); values.push(input.fileUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE contracts SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM contracts WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/customer.ts b/.benchmark/contract-mgmt/backend/src/services/customer.ts new file mode 100644 index 0000000..0c7ebd3 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/customer.ts @@ -0,0 +1,57 @@ +// Auto-generated Customer service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js"; + +export class CustomerService { + /** List all customers */ + list(): Customer[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customer | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomerInput): Customer { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "tags", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, input.tags ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomerInput): Customer | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + if (input.tags !== undefined) { sets.push("tags = ?"); values.push(input.tags); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/operation_log.ts b/.benchmark/contract-mgmt/backend/src/services/operation_log.ts new file mode 100644 index 0000000..83854d0 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/operation_log.ts @@ -0,0 +1,54 @@ +// Auto-generated OperationLogs service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { OperationLogs, CreateOperationLogsInput, UpdateOperationLogsInput } from "../types/index.js"; + +export class OperationLogsService { + /** List all operation_logs */ + list(): OperationLogs[] { + return queryAll("SELECT * FROM operation_logs ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): OperationLogs | undefined { + return queryOne("SELECT * FROM operation_logs WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateOperationLogsInput): OperationLogs { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now]; + + execute(`INSERT INTO operation_logs (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateOperationLogsInput): OperationLogs | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE operation_logs SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM operation_logs WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/reminder.ts b/.benchmark/contract-mgmt/backend/src/services/reminder.ts new file mode 100644 index 0000000..8c19dd5 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/reminder.ts @@ -0,0 +1,56 @@ +// Auto-generated Reminder service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Reminder, CreateReminderInput, UpdateReminderInput } from "../types/index.js"; + +export class ReminderService { + /** List all reminders */ + list(): Reminder[] { + return queryAll("SELECT * FROM reminders ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Reminder | undefined { + return queryOne("SELECT * FROM reminders WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateReminderInput): Reminder { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "entity_type", "entity_id", "remind_at", "message", "sent", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.entityType ?? null, input.entityId ?? null, input.remindAt ?? null, input.message ?? null, input.sent ?? null, now, now]; + + execute(`INSERT INTO reminders (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateReminderInput): Reminder | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.entityType !== undefined) { sets.push("entity_type = ?"); values.push(input.entityType); } + if (input.entityId !== undefined) { sets.push("entity_id = ?"); values.push(input.entityId); } + if (input.remindAt !== undefined) { sets.push("remind_at = ?"); values.push(input.remindAt); } + if (input.message !== undefined) { sets.push("message = ?"); values.push(input.message); } + if (input.sent !== undefined) { sets.push("sent = ?"); values.push(input.sent); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE reminders SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM reminders WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/services/stat.ts b/.benchmark/contract-mgmt/backend/src/services/stat.ts new file mode 100644 index 0000000..1d4080d --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/services/stat.ts @@ -0,0 +1,54 @@ +// Auto-generated Stats service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Stats, CreateStatsInput, UpdateStatsInput } from "../types/index.js"; + +export class StatsService { + /** List all stats */ + list(): Stats[] { + return queryAll("SELECT * FROM stats ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Stats | undefined { + return queryOne("SELECT * FROM stats WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStatsInput): Stats { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "metric", "value", "period", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.metric ?? null, input.value ?? null, input.period ?? null, now, now]; + + execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStatsInput): Stats | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.metric !== undefined) { sets.push("metric = ?"); values.push(input.metric); } + if (input.value !== undefined) { sets.push("value = ?"); values.push(input.value); } + if (input.period !== undefined) { sets.push("period = ?"); values.push(input.period); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM stats WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/types/fastify.d.ts b/.benchmark/contract-mgmt/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/contract-mgmt/backend/src/types/index.ts b/.benchmark/contract-mgmt/backend/src/types/index.ts new file mode 100644 index 0000000..a14e7a2 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/types/index.ts @@ -0,0 +1,258 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface OperationLogs { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateContractInput { + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface CreateApprovalInput { + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; +} + +export interface CreateCustomerInput { + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface CreateReminderInput { + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; +} + +export interface CreateStatsInput { + userId?: string; + metric: string; + value?: number; + period?: string; +} + +export interface CreateOperationLogsInput { + userId?: string; + title: string; + description?: string; + data?: Record; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateContractInput { + userId?: string; + title?: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; +} + +export interface UpdateApprovalInput { + userId?: string; + entityType?: string; + entityId?: string; + applicantId?: string; + status?: string; + formData?: Record; +} + +export interface UpdateCustomerInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; +} + +export interface UpdateReminderInput { + userId?: string; + entityType?: string; + entityId?: string; + remindAt?: string; + message?: string; + sent?: boolean; +} + +export interface UpdateStatsInput { + userId?: string; + metric?: string; + value?: number; + period?: string; +} + +export interface UpdateOperationLogsInput { + userId?: string; + title?: string; + description?: string; + data?: Record; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/contract-mgmt/backend/src/types/sql.js.d.ts b/.benchmark/contract-mgmt/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/contract-mgmt/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/contract-mgmt/backend/tsconfig.json b/.benchmark/contract-mgmt/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/contract-mgmt/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/electron/README.md b/.benchmark/contract-mgmt/electron/README.md new file mode 100644 index 0000000..05c1798 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/README.md @@ -0,0 +1,82 @@ +# oaflow โ€” Desktop + +> Electron desktop application wrapping the oaflow fullstack web project. + +## Architecture + +``` +Web Fullstack Project + ร— + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** โ€” Window management, menu, tray, IPC, auto-update +- **Preload** โ€” Secure bridge between main and renderer +- **Renderer** โ€” Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +โ”œโ”€โ”€ main.ts โ€” Main process (BrowserWindow, Menu, lifecycle) +โ”œโ”€โ”€ preload.ts โ€” Secure IPC bridge (contextBridge) +โ”œโ”€โ”€ ipc.ts โ€” IPC handlers (dialogs, store, notifications) +โ”œโ”€โ”€ tray.ts โ€” System tray +โ”œโ”€โ”€ updater.ts โ€” Auto-update (electron-updater) +โ””โ”€โ”€ utils.ts โ€” Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/contract-mgmt/electron/assets/icon.png b/.benchmark/contract-mgmt/electron/assets/icon.png new file mode 100644 index 0000000..554e8a3 Binary files /dev/null and b/.benchmark/contract-mgmt/electron/assets/icon.png differ diff --git a/.benchmark/contract-mgmt/electron/electron-builder.yml b/.benchmark/contract-mgmt/electron/electron-builder.yml new file mode 100644 index 0000000..cc3c4e8 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml โ€” Auto-generated by SF-06 + +appId: com.oaflow.desktop +productName: oaflow +copyright: "Copyright ยฉ 2026 oaflow" + +# โ”€โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +directories: + output: release + buildResources: assets + +# โ”€โ”€โ”€ Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# โ”€โ”€โ”€ Extra Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# โ”€โ”€โ”€ macOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# โ”€โ”€โ”€ Windows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: oaflow + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: oaflow + +# โ”€โ”€โ”€ Linux โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: oaflow + Comment: oaflow Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# โ”€โ”€โ”€ Auto Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +publish: + provider: generic + url: https://releases.example.com/oaflow/ + channel: latest diff --git a/.benchmark/contract-mgmt/electron/electron/ipc.ts b/.benchmark/contract-mgmt/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // โ”€โ”€ App Info โ”€โ”€ + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // โ”€โ”€ Window Controls โ”€โ”€ + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // โ”€โ”€ File Dialogs โ”€โ”€ + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // โ”€โ”€ Notifications โ”€โ”€ + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // โ”€โ”€ Shell โ”€โ”€ + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // โ”€โ”€ Persistent Store โ”€โ”€ + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/contract-mgmt/electron/electron/main.ts b/.benchmark/contract-mgmt/electron/electron/main.ts new file mode 100644 index 0000000..ba02088 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "oaflow"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// โ”€โ”€โ”€ Create Window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // โ”€โ”€ Load content โ”€โ”€ + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // โ”€โ”€ Show when ready โ”€โ”€ + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // โ”€โ”€ External links โ†’ default browser โ”€โ”€ + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // โ”€โ”€ Prevent navigation away โ”€โ”€ + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// โ”€โ”€โ”€ Icon Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// โ”€โ”€โ”€ API Server (Production) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// โ”€โ”€โ”€ Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// โ”€โ”€โ”€ App Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed โ€” continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/contract-mgmt/electron/electron/preload.ts b/.benchmark/contract-mgmt/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// โ”€โ”€โ”€ Expose safe API to renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +contextBridge.exposeInMainWorld("electronAPI", { + // โ”€โ”€ App Info โ”€โ”€ + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // โ”€โ”€ Window Controls โ”€โ”€ + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // โ”€โ”€ File Dialogs โ”€โ”€ + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // โ”€โ”€ Notifications โ”€โ”€ + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // โ”€โ”€ Shell โ”€โ”€ + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // โ”€โ”€ Store โ”€โ”€ + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // โ”€โ”€ Updates โ”€โ”€ + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // โ”€โ”€ Update Events โ”€โ”€ + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // โ”€โ”€ Remove listener โ”€โ”€ + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// โ”€โ”€โ”€ Type declarations for renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/contract-mgmt/electron/electron/tray.ts b/.benchmark/contract-mgmt/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/contract-mgmt/electron/electron/updater.ts b/.benchmark/contract-mgmt/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/contract-mgmt/electron/electron/utils.ts b/.benchmark/contract-mgmt/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// โ”€โ”€โ”€ Dev mode detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function isDev(): boolean { + return !app.isPackaged; +} + +// โ”€โ”€โ”€ Web URL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// โ”€โ”€โ”€ API Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// โ”€โ”€โ”€ Get API port from env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/contract-mgmt/electron/entitlements.mac.plist b/.benchmark/contract-mgmt/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/contract-mgmt/electron/package.json b/.benchmark/contract-mgmt/electron/package.json new file mode 100644 index 0000000..653d034 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "oaflow-desktop", + "version": "1.0.0", + "private": true, + "description": "oaflow โ€” Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "oaflow", + "appId": "com.oaflow.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/electron/tsconfig.json b/.benchmark/contract-mgmt/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/contract-mgmt/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/app/approvals/page.tsx b/.benchmark/contract-mgmt/frontend/app/approvals/page.tsx new file mode 100644 index 0000000..43808c0 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/approvals/page.tsx @@ -0,0 +1,35 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const approvals = [ + { id: "a1", type: "่ฏทๅ‡", applicant: "ๅผ ไธ‰", status: "pending", date: "2026-06-05" }, + { id: "a2", type: "ๆŠฅ้”€", applicant: "ๆŽๅ››", status: "approved", date: "2026-06-04" }, + { id: "a3", type: "ๅŠ ็ญ", applicant: "็Ž‹ไบ”", status: "rejected", date: "2026-06-03" }, +]; +const statusMap: Record = { + pending: { label: "ๅพ…ๅฎกๆ‰น", color: "bg-yellow-100 text-yellow-700" }, + approved: { label: "ๅทฒ้€š่ฟ‡", color: "bg-green-100 text-green-700" }, + rejected: { label: "ๅทฒ้ฉณๅ›ž", color: "bg-red-100 text-red-700" }, +}; + +export default function ApprovalPage() { + return ( +
+

ๆˆ‘็š„ๅฎกๆ‰น

+ {approvals.map(a => { + const s = statusMap[a.status]; + return ( + +
+
+

{a.type}็”ณ่ฏท โ€” {a.applicant}

+

{a.date}

+
+ {s.label} +
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/app/contracts/page.tsx b/.benchmark/contract-mgmt/frontend/app/contracts/page.tsx new file mode 100644 index 0000000..902f8d6 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/contracts/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useContracts } from "@/hooks/useContracts"; + +export default function PagePage() { + const { data, loading, error, refetch } = useContracts(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅˆๅŒๅˆ›ๅปบ

+

ๅˆๅŒๅˆ›ๅปบ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/customers/page.tsx b/.benchmark/contract-mgmt/frontend/app/customers/page.tsx new file mode 100644 index 0000000..a8cba9e --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/customers/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useCustomers } from "@/hooks/useCustomers"; + +export default function PagePage() { + const { data, loading, error, refetch } = useCustomers(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅฎขๆˆท็ฎก็†

+

ๅฎขๆˆท็ฎก็†

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/globals.css b/.benchmark/contract-mgmt/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/contract-mgmt/frontend/app/layout.tsx b/.benchmark/contract-mgmt/frontend/app/layout.tsx new file mode 100644 index 0000000..6faad51 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "OAFlow", + description: "ไธ€ๆฌพๆ”ฏๆŒๅฎกๆ‰นๆตใ€่€ƒๅ‹ค็ฎก็†ใ€้ƒจ้—จๅไฝœ็š„ไผไธšๅŠžๅ…ฌ่‡ชๅŠจๅŒ–็ณป็ปŸใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx b/.benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx new file mode 100644 index 0000000..d8dce63 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/operation_logs/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆ“ไฝœๆ—ฅๅฟ—

+

ๆ“ไฝœๆ—ฅๅฟ—

+
+ +
+ + { +

ๆ“ไฝœๆ—ฅๅฟ— ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /operation_logs

+
} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/page.tsx b/.benchmark/contract-mgmt/frontend/app/page.tsx new file mode 100644 index 0000000..ae4e833 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

OAFlow

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ๅˆๅŒๅˆ›ๅปบ + + + ๐Ÿ“… + ๅˆๅŒๅฎกๆ‰น + + + ๐Ÿ“ + ๅˆๅŒๅˆฐๆœŸๆ้†’ + + + ๐Ÿ“ธ + ๅฎขๆˆท็ฎก็† + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ OAFlow

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/reminders/page.tsx b/.benchmark/contract-mgmt/frontend/app/reminders/page.tsx new file mode 100644 index 0000000..156651d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/reminders/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useReminders } from "@/hooks/useReminders"; + +export default function PagePage() { + const { data, loading, error, refetch } = useReminders(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅˆๅŒๅˆฐๆœŸๆ้†’

+

ๅˆๅŒๅˆฐๆœŸๆ้†’

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/app/stats/page.tsx b/.benchmark/contract-mgmt/frontend/app/stats/page.tsx new file mode 100644 index 0000000..fba1ace --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/app/stats/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅˆๅŒ้‡‘้ข็ปŸ่ฎก

+

ๅˆๅŒ้‡‘้ข็ปŸ่ฎก

+
+ +
+ + { +

ๅˆๅŒ้‡‘้ข็ปŸ่ฎก ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /stats

+
} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx b/.benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx b/.benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Button.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Card.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx b/.benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Input.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/components/ui/Modal.tsx b/.benchmark/contract-mgmt/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useApprovals.ts b/.benchmark/contract-mgmt/frontend/hooks/useApprovals.ts new file mode 100644 index 0000000..0163a46 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useApprovals.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for approvals +type Approval = Record; + +/** + * Fetch and manage approvals data. + */ +export function useApprovals() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/approvals`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useAuth.ts b/.benchmark/contract-mgmt/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useContracts.ts b/.benchmark/contract-mgmt/frontend/hooks/useContracts.ts new file mode 100644 index 0000000..9fbf845 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useContracts.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for contracts +type Contract = Record; + +/** + * Fetch and manage contracts data. + */ +export function useContracts() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/contracts`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useCustomers.ts b/.benchmark/contract-mgmt/frontend/hooks/useCustomers.ts new file mode 100644 index 0000000..8669d02 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useCustomers.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for customers +type Customer = Record; + +/** + * Fetch and manage customers data. + */ +export function useCustomers() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/customers`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useDebounce.ts b/.benchmark/contract-mgmt/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useForm.ts b/.benchmark/contract-mgmt/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/contract-mgmt/frontend/hooks/useReminders.ts b/.benchmark/contract-mgmt/frontend/hooks/useReminders.ts new file mode 100644 index 0000000..7ce891d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/hooks/useReminders.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for reminders +type Reminder = Record; + +/** + * Fetch and manage reminders data. + */ +export function useReminders() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/reminders`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/contract-mgmt/frontend/next.config.ts b/.benchmark/contract-mgmt/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/contract-mgmt/frontend/package.json b/.benchmark/contract-mgmt/frontend/package.json new file mode 100644 index 0000000..696da02 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "oaflow", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/postcss.config.js b/.benchmark/contract-mgmt/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/contract-mgmt/frontend/services/api.ts b/.benchmark/contract-mgmt/frontend/services/api.ts new file mode 100644 index 0000000..db22ccf --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for OAFlow + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/contract-mgmt/frontend/services/approvals.ts b/.benchmark/contract-mgmt/frontend/services/approvals.ts new file mode 100644 index 0000000..65620ab --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/approvals.ts @@ -0,0 +1,10 @@ +// Auto-generated approvals service +import { api } from "./api"; + +export function get() { + return api.get("/api/approvals"); +} + +export function post(data: Record) { + return api.post("/api/approvals", data); +} diff --git a/.benchmark/contract-mgmt/frontend/services/auth.ts b/.benchmark/contract-mgmt/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/contract-mgmt/frontend/services/contracts.ts b/.benchmark/contract-mgmt/frontend/services/contracts.ts new file mode 100644 index 0000000..a2c8c24 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/contracts.ts @@ -0,0 +1,10 @@ +// Auto-generated contracts service +import { api } from "./api"; + +export function get() { + return api.get("/api/contracts"); +} + +export function post(data: Record) { + return api.post("/api/contracts", data); +} diff --git a/.benchmark/contract-mgmt/frontend/services/customers.ts b/.benchmark/contract-mgmt/frontend/services/customers.ts new file mode 100644 index 0000000..c0e2a5e --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/customers.ts @@ -0,0 +1,10 @@ +// Auto-generated customers service +import { api } from "./api"; + +export function get() { + return api.get("/api/customers"); +} + +export function post(data: Record) { + return api.post("/api/customers", data); +} diff --git a/.benchmark/contract-mgmt/frontend/services/reminders.ts b/.benchmark/contract-mgmt/frontend/services/reminders.ts new file mode 100644 index 0000000..6bfa9b5 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/services/reminders.ts @@ -0,0 +1,10 @@ +// Auto-generated reminders service +import { api } from "./api"; + +export function get() { + return api.get("/api/reminders"); +} + +export function post(data: Record) { + return api.post("/api/reminders", data); +} diff --git a/.benchmark/contract-mgmt/frontend/tailwind.config.ts b/.benchmark/contract-mgmt/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/contract-mgmt/frontend/tsconfig.json b/.benchmark/contract-mgmt/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/frontend/types/index.ts b/.benchmark/contract-mgmt/frontend/types/index.ts new file mode 100644 index 0000000..b49e42e --- /dev/null +++ b/.benchmark/contract-mgmt/frontend/types/index.ts @@ -0,0 +1,106 @@ +// Auto-generated types for OAFlow + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Contract { + id?: string; + userId: string; + title: string; + partyA?: string; + partyB?: string; + amount?: number; + signedAt?: string; + expiresAt?: string; + status?: string; + fileUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Approval { + id?: string; + userId: string; + entityType: string; + entityId?: string; + applicantId: string; + status?: string; + formData?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Customer { + id?: string; + userId: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface Reminder { + id?: string; + userId: string; + entityType: string; + entityId?: string; + remindAt: string; + message?: string; + sent?: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface Stats { + id: string; + userId?: string; + metric: string; + value?: number; + period?: string; + recordedAt?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface OperationLogs { + id: string; + userId?: string; + title: string; + description?: string; + status?: string; + data?: Record; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/prd.json b/.benchmark/contract-mgmt/prd.json new file mode 100644 index 0000000..adee471 --- /dev/null +++ b/.benchmark/contract-mgmt/prd.json @@ -0,0 +1 @@ +{"projectName":"OAFlow","chineseName":"ไผไธšๅŠžๅ…ฌ่‡ชๅŠจๅŒ–","domain":"enterprise","matchConfidence":"high","summary":"ไธ€ๆฌพๆ”ฏๆŒๅฎกๆ‰นๆตใ€่€ƒๅ‹ค็ฎก็†ใ€้ƒจ้—จๅไฝœ็š„ไผไธšๅŠžๅ…ฌ่‡ชๅŠจๅŒ–็ณป็ปŸใ€‚","personas":[{"name":"ๆ™ฎ้€šๅ‘˜ๅทฅ","description":"้œ€่ฆๆ—ฅๅธธ่€ƒๅ‹คๆ‰“ๅกใ€ๅ‘่ตทๅฎกๆ‰น็š„ไผไธšๅ‘˜ๅทฅ","painPoints":["ๅฎกๆ‰นๆต็จ‹็น็","่€ƒๅ‹คๅผ‚ๅธธๅค„็†้บป็ƒฆ"]},{"name":"้ƒจ้—จไธป็ฎก","description":"่ดŸ่ดฃๅฎกๆ‰นใ€ๅ›ข้˜Ÿ็ฎก็†็š„็ฎก็†่€…","painPoints":["ๅฎกๆ‰นๅ †็งฏ","ๅ›ข้˜Ÿๅ‡บๅ‹คๆ— ๆณ•ไธ€็›ฎไบ†็„ถ"]},{"name":"HR/่กŒๆ”ฟ","description":"่ดŸ่ดฃ่€ƒๅ‹ค็ปŸ่ฎกใ€ไบบไบ‹็ฎก็†็š„HR","painPoints":["่€ƒๅ‹คๆ•ฐๆฎๆ‰‹ๅทฅ็ปŸ่ฎก","่ทจ้ƒจ้—จๆต็จ‹ๆททไนฑ"]}],"userStories":[{"id":"US-001","as":"ๆ™ฎ้€šๅ‘˜ๅทฅ","want":"ๅˆๅŒๅˆ›ๅปบ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅฎกๆ‰นๆต็จ‹็น็","priority":"P0"},{"id":"US-002","as":"ๆ™ฎ้€šๅ‘˜ๅทฅ","want":"ๅˆๅŒๅฎกๆ‰น","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅฎกๆ‰นๆต็จ‹็น็","priority":"P0"},{"id":"US-003","as":"ๆ™ฎ้€šๅ‘˜ๅทฅ","want":"ๅˆๅŒๅˆฐๆœŸๆ้†’","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅฎกๆ‰นๆต็จ‹็น็","priority":"P0"},{"id":"US-004","as":"้ƒจ้—จไธป็ฎก","want":"ๅˆๅŒๅˆ›ๅปบ","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅฎกๆ‰นๅ †็งฏ","priority":"P0"},{"id":"US-005","as":"้ƒจ้—จไธป็ฎก","want":"ๅˆๅŒๅฎกๆ‰น","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅฎกๆ‰นๅ †็งฏ","priority":"P0"},{"id":"US-006","as":"้ƒจ้—จไธป็ฎก","want":"ๅˆๅŒๅˆฐๆœŸๆ้†’","soThat":"่งฃๅ†ณ็—›็‚น๏ผšๅฎกๆ‰นๅ †็งฏ","priority":"P0"},{"id":"US-007","as":"HR/่กŒๆ”ฟ","want":"ๅˆๅŒๅˆ›ๅปบ","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่€ƒๅ‹คๆ•ฐๆฎๆ‰‹ๅทฅ็ปŸ่ฎก","priority":"P0"},{"id":"US-008","as":"HR/่กŒๆ”ฟ","want":"ๅˆๅŒๅฎกๆ‰น","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่€ƒๅ‹คๆ•ฐๆฎๆ‰‹ๅทฅ็ปŸ่ฎก","priority":"P0"},{"id":"US-009","as":"HR/่กŒๆ”ฟ","want":"ๅˆๅŒๅˆฐๆœŸๆ้†’","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่€ƒๅ‹คๆ•ฐๆฎๆ‰‹ๅทฅ็ปŸ่ฎก","priority":"P0"}],"mvpScope":{"description":"MVP ่š็„ฆ ๅˆๅŒๅˆ›ๅปบใ€ๅˆๅŒๅฎกๆ‰นใ€ๅˆๅŒๅˆฐๆœŸๆ้†’ใ€ๅฎขๆˆท็ฎก็†ใ€ๅˆๅŒ้‡‘้ข็ปŸ่ฎกใ€ๆ“ไฝœๆ—ฅๅฟ—๏ผŒๅŒ…ๅซ้กต้ข๏ผš้ฆ–้กตใ€ๅˆๅŒๅˆ›ๅปบใ€ๅˆๅŒๅฎกๆ‰นใ€ๅˆๅŒๅˆฐๆœŸๆ้†’ใ€ๅฎขๆˆท็ฎก็†","features":["ๅˆๅŒๅˆ›ๅปบ","ๅˆๅŒๅฎกๆ‰น","ๅˆๅŒๅˆฐๆœŸๆ้†’","ๅฎขๆˆท็ฎก็†","ๅˆๅŒ้‡‘้ข็ปŸ่ฎก","ๆ“ไฝœๆ—ฅๅฟ—"],"pages":["้ฆ–้กต","ๅˆๅŒๅˆ›ๅปบ","ๅˆๅŒๅฎกๆ‰น","ๅˆๅŒๅˆฐๆœŸๆ้†’","ๅฎขๆˆท็ฎก็†"],"estimatedWeeks":3},"features":[{"name":"ๅˆๅŒๅˆ›ๅปบ","description":"ๅˆๅŒๅˆ›ๅปบ","priority":"P0"},{"name":"ๅˆๅŒๅฎกๆ‰น","description":"ๅˆๅŒๅฎกๆ‰น","priority":"P0"},{"name":"ๅˆๅŒๅˆฐๆœŸๆ้†’","description":"ๅˆๅŒๅˆฐๆœŸๆ้†’","priority":"P0"},{"name":"ๅฎขๆˆท็ฎก็†","description":"ๅฎขๆˆท็ฎก็†","priority":"P0"},{"name":"ๅˆๅŒ้‡‘้ข็ปŸ่ฎก","description":"ๅˆๅŒ้‡‘้ข็ปŸ่ฎก","priority":"P0"},{"name":"ๆ“ไฝœๆ—ฅๅฟ—","description":"ๆ“ไฝœๆ—ฅๅฟ—","priority":"P0"}],"pages":[{"name":"้ฆ–้กต","route":"/home","description":"ๅบ”็”จ้ฆ–้กต"},{"name":"ๅˆๅŒๅˆ›ๅปบ","route":"/contracts","description":"ๅˆๅŒๅˆ›ๅปบ"},{"name":"ๅˆๅŒๅฎกๆ‰น","route":"/approvals","description":"ๅˆๅŒๅฎกๆ‰น"},{"name":"ๅˆๅŒๅˆฐๆœŸๆ้†’","route":"/reminders","description":"ๅˆๅŒๅˆฐๆœŸๆ้†’"},{"name":"ๅฎขๆˆท็ฎก็†","route":"/customers","description":"ๅฎขๆˆท็ฎก็†"},{"name":"ๅˆๅŒ้‡‘้ข็ปŸ่ฎก","route":"/stats","description":"ๅˆๅŒ้‡‘้ข็ปŸ่ฎก"},{"name":"ๆ“ไฝœๆ—ฅๅฟ—","route":"/operation_logs","description":"ๆ“ไฝœๆ—ฅๅฟ—"}],"apiRequirements":[{"method":"GET","path":"/api/contracts","description":"่Žทๅ–ๅˆๅŒๅˆ›ๅปบๅˆ—่กจ"},{"method":"POST","path":"/api/contracts","description":"ๅˆ›ๅปบๅˆๅŒๅˆ›ๅปบ"},{"method":"GET","path":"/api/approvals","description":"่Žทๅ–ๅˆๅŒๅฎกๆ‰นๅˆ—่กจ"},{"method":"POST","path":"/api/approvals","description":"ๅˆ›ๅปบๅˆๅŒๅฎกๆ‰น"},{"method":"GET","path":"/api/reminders","description":"่Žทๅ–ๅˆๅŒๅˆฐๆœŸๆ้†’ๅˆ—่กจ"},{"method":"POST","path":"/api/reminders","description":"ๅˆ›ๅปบๅˆๅŒๅˆฐๆœŸๆ้†’"},{"method":"GET","path":"/api/customers","description":"่Žทๅ–ๅฎขๆˆท็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/customers","description":"ๅˆ›ๅปบๅฎขๆˆท็ฎก็†"},{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}],"techConstraints":{"platforms":["desktop"],"recommendedStack":"React Native / Flutter๏ผˆ่ทจๅนณๅฐ๏ผ‰","considerations":["ๅปบ่ฎฎ่ทจๅนณๅฐๆก†ๆžถๅ‡ๅฐ‘ๅผ€ๅ‘ๆˆๆœฌ","้œ€ๆ”ฏๆŒ RBAC ๆƒ้™ๆจกๅž‹","้œ€่€ƒ่™‘ไผไธš SSO ้›†ๆˆ"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"้กน็›ฎ่„šๆ‰‹ๆžถๆญๅปบ","description":"ๅˆๅง‹ๅŒ– OAFlow ้กน็›ฎ็ป“ๆž„๏ผŒ้…็ฝฎๆž„ๅปบๅทฅๅ…ทๅ’Œ CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"ๆ•ฐๆฎๅบ“่ฎพ่ฎกไธŽๅˆๅง‹ๅŒ–","description":"่ฎพ่ฎกๆ•ฐๆฎๆจกๅž‹๏ผŒๅˆ›ๅปบๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌ","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"้กต้ขๅผ€ๅ‘๏ผš้ฆ–้กต","description":"ๅผ€ๅ‘ ้ฆ–้กต ้กต้ข๏ผˆ/home๏ผ‰๏ผšๅบ”็”จ้ฆ–้กต","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"้กต้ขๅผ€ๅ‘๏ผšๅˆๅŒๅˆ›ๅปบ","description":"ๅผ€ๅ‘ ๅˆๅŒๅˆ›ๅปบ ้กต้ข๏ผˆ/contracts๏ผ‰๏ผšๅˆๅŒๅˆ›ๅปบ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"้กต้ขๅผ€ๅ‘๏ผšๅˆๅŒๅฎกๆ‰น","description":"ๅผ€ๅ‘ ๅˆๅŒๅฎกๆ‰น ้กต้ข๏ผˆ/approvals๏ผ‰๏ผšๅˆๅŒๅฎกๆ‰น","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"้กต้ขๅผ€ๅ‘๏ผšๅˆๅŒๅˆฐๆœŸๆ้†’","description":"ๅผ€ๅ‘ ๅˆๅŒๅˆฐๆœŸๆ้†’ ้กต้ข๏ผˆ/reminders๏ผ‰๏ผšๅˆๅŒๅˆฐๆœŸๆ้†’","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API ๅผ€ๅ‘๏ผšGET /api/contracts","description":"่Žทๅ–ๅˆๅŒๅˆ›ๅปบๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API ๅผ€ๅ‘๏ผšPOST /api/contracts","description":"ๅˆ›ๅปบๅˆๅŒๅˆ›ๅปบ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API ๅผ€ๅ‘๏ผšGET /api/approvals","description":"่Žทๅ–ๅˆๅŒๅฎกๆ‰นๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API ๅผ€ๅ‘๏ผšPOST /api/approvals","description":"ๅˆ›ๅปบๅˆๅŒๅฎกๆ‰น","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"ๅ‰ๅŽ็ซฏ่”่ฐƒ","description":"ๅ‰็ซฏ้กต้ขไธŽๅŽ็ซฏ API ้›†ๆˆ่”่ฐƒ","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"ๆต‹่ฏ•ไธŽไฟฎๅค","description":"ๅŠŸ่ƒฝๆต‹่ฏ•ใ€Bug ไฟฎๅคใ€ๆ€ง่ƒฝไผ˜ๅŒ–","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T13:43:26.870Z","inputLength":48,"domainMatchCount":1,"extractedFeatureCount":6,"extractedPageCount":7,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/README.md b/.benchmark/contract-mgmt/release/release/README.md new file mode 100644 index 0000000..2688299 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/README.md @@ -0,0 +1,33 @@ +# oaflow โ€” Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T13:43:27.214Z + +## Directory Structure + +``` +release/ +โ”œโ”€โ”€ version.json โ€” Version metadata +โ”œโ”€โ”€ build-info.json โ€” Build environment info +โ”œโ”€โ”€ README.md โ€” This file +โ”œโ”€โ”€ manifests/ +โ”‚ โ””โ”€โ”€ manifest.json โ€” Release manifest with file hashes +โ”œโ”€โ”€ checksums/ +โ”‚ โ””โ”€โ”€ checksums.txt โ€” SHA256 checksums for verification +โ”œโ”€โ”€ release-notes/ +โ”‚ โ””โ”€โ”€ release-notes.md โ€” Human-readable release notes +โ”œโ”€โ”€ windows/ โ€” Windows installers +โ”œโ”€โ”€ macos/ โ€” macOS disk images +โ””โ”€โ”€ linux/ โ€” Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/build-info.json b/.benchmark/contract-mgmt/release/release/build-info.json new file mode 100644 index 0000000..ef7691f --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T13:43:27.214Z", + "domain": "unknown", + "entityCount": 7, + "routeCount": 1, + "screenCount": 6, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "oaflow", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/checksums/checksums.txt b/.benchmark/contract-mgmt/release/release/checksums/checksums.txt new file mode 100644 index 0000000..a4f150a --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +f5b8f0725308ab4c22da6343cc15692a1803714e53becdbb5151b519c19fab2b windows/oaflow-setup-0.1.0.exe +47a4a3cd0533b44e0253132d04c92fa9c5e657c856ef762520db11c12f0d880a windows/oaflow-0.1.0-portable.exe +248f6af0f2e07555102549ea6624deb2e5c73167db17fd7dbc9fe7046603b53f macos/oaflow-0.1.0.dmg +7b78cfee2917c9053a33bc386d19ff7531fc94366bd5530764399bbbc0a81eaa macos/oaflow-0.1.0-arm64.dmg +8387fdf21838ffff29b78bb8af70a9a0291f3e8f00c3a192df8952baf92b28da linux/oaflow-0.1.0.AppImage +7c0731541f4667ee7c50a31ef4731bf6bf00d30d9e3bc351f34ca21b855ad828 linux/oaflow_0.1.0_amd64.deb diff --git a/.benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage b/.benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage new file mode 100644 index 0000000..a457331 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/linux/oaflow-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb b/.benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb new file mode 100644 index 0000000..419a570 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/linux/oaflow_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg new file mode 100644 index 0000000..38a2028 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg new file mode 100644 index 0000000..9449d3b --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/macos/oaflow-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/manifests/manifest.json b/.benchmark/contract-mgmt/release/release/manifests/manifest.json new file mode 100644 index 0000000..479662f --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "oaflow", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T13:43:27.214Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/oaflow-setup-0.1.0.exe", + "size": 141, + "sha256": "f5b8f0725308ab4c22da6343cc15692a1803714e53becdbb5151b519c19fab2b" + }, + { + "path": "windows/oaflow-0.1.0-portable.exe", + "size": 145, + "sha256": "47a4a3cd0533b44e0253132d04c92fa9c5e657c856ef762520db11c12f0d880a" + }, + { + "path": "macos/oaflow-0.1.0.dmg", + "size": 137, + "sha256": "248f6af0f2e07555102549ea6624deb2e5c73167db17fd7dbc9fe7046603b53f" + }, + { + "path": "macos/oaflow-0.1.0-arm64.dmg", + "size": 139, + "sha256": "7b78cfee2917c9053a33bc386d19ff7531fc94366bd5530764399bbbc0a81eaa" + }, + { + "path": "linux/oaflow-0.1.0.AppImage", + "size": 130, + "sha256": "8387fdf21838ffff29b78bb8af70a9a0291f3e8f00c3a192df8952baf92b28da" + }, + { + "path": "linux/oaflow_0.1.0_amd64.deb", + "size": 140, + "sha256": "7c0731541f4667ee7c50a31ef4731bf6bf00d30d9e3bc351f34ca21b855ad828" + } + ], + "totalFiles": 6, + "totalSize": 832 +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/release-notes/release-notes.md b/.benchmark/contract-mgmt/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..885ead4 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/release-notes/release-notes.md @@ -0,0 +1,56 @@ +# oaflow v0.1.0 + +> Release Builder Agent โ€” SF-07 +> Generated: 2026-06-05 13:43:27 UTC + +## Version + +- **Name:** oaflow +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- approvals +- auth +- contracts +- customers +- operation_logs +- reminders +- stats + +## Build Info + +- **Build Time:** 2026-06-05T13:43:27.214Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- โœ… windows +- โœ… macos +- โœ… linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/version.json b/.benchmark/contract-mgmt/release/release/version.json new file mode 100644 index 0000000..1c5bd28 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "oaflow", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe b/.benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe new file mode 100644 index 0000000..c5888cc --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/windows/oaflow-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe b/.benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe new file mode 100644 index 0000000..e0276c1 --- /dev/null +++ b/.benchmark/contract-mgmt/release/release/windows/oaflow-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] oaflow v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T13:43:27.213Z diff --git a/.benchmark/contract-mgmt/summary.json b/.benchmark/contract-mgmt/summary.json new file mode 100644 index 0000000..9ba6ab6 --- /dev/null +++ b/.benchmark/contract-mgmt/summary.json @@ -0,0 +1,43 @@ +{ + "input": "ๅผ€ๅ‘ไธ€ไธชๅˆๅŒ็ฎก็†ๆกŒ้ขๅทฅๅ…ท๏ผŒๆ”ฏๆŒๅˆๅŒๅˆ›ๅปบใ€ๅˆๅŒๅฎกๆ‰นใ€ๅˆๅŒๅˆฐๆœŸๆ้†’ใ€ๅฎขๆˆท็ฎก็†ใ€ๅˆๅŒ้‡‘้ข็ปŸ่ฎกใ€ๆ“ไฝœๆ—ฅๅฟ—", + "projectName": "OAFlow", + "domain": "enterprise", + "stages": { + "prd": { + "ms": 51 + }, + "arch": { + "ms": 55, + "modules": 9 + }, + "frontend": { + "ms": 51, + "files": 35, + "pass": true + }, + "backend": { + "ms": 54, + "files": 32, + "pass": true + }, + "fullstack": { + "ms": 57, + "files": 79, + "pass": true + }, + "electron": { + "ms": 49, + "files": 12, + "pass": true + }, + "release": { + "ms": 54, + "files": 12, + "pass": true + } + }, + "totalMs": 371, + "totalFiles": 170, + "allPass": true, + "outputDir": "/Users/a1234/.openclaw/workspace/.benchmark/contract-mgmt" +} \ No newline at end of file diff --git a/.benchmark/course/arch.json b/.benchmark/course/arch.json new file mode 100644 index 0000000..67c3b67 --- /dev/null +++ b/.benchmark/course/arch.json @@ -0,0 +1 @@ +{"projectName":"EduPlatform","domain":"education","contract":{"projectName":"EduPlatform","domain":"education","entities":[{"name":"User","table":"users","description":"็ณป็ปŸ็”จๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin","instructor"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[]},{"name":"Courses","table":"courses","description":"่ฏพ็จ‹ๅ‘ๅธƒ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"instructor","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"price","type":"DECIMAL(10,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"cover_url","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"category","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Chapters","table":"chapters","description":"็ซ ่Š‚็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"course_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"courses","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"sort_order","type":"INTEGER","required":false,"isPrimary":false,"isAuto":true},{"name":"duration_min","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"courses","via":"course_id"}]},{"name":"Students","table":"students","description":"ๅญฆๅ‘˜่ฟ›ๅบฆ่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(64)","required":true,"isPrimary":false,"isAuto":false},{"name":"email","type":"VARCHAR(256)","required":false,"isPrimary":false,"isAuto":false},{"name":"enrolled_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"progress","type":"DECIMAL(5,2)","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"Assignments","table":"assignments","description":"ไฝœไธšๆ‰นๆ”น่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"course_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"courses","fkColumn":"id"},{"name":"title","type":"VARCHAR(256)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"due_date","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"max_score","type":"INTEGER","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"courses","via":"course_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","cover_url":"coverUrl","course_id":"courseId","sort_order":"sortOrder","duration_min":"durationMin","enrolled_at":"enrolledAt","due_date":"dueDate","max_score":"maxScore","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","remind_at":"remindAt","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","coverUrl":"cover_url","courseId":"course_id","sortOrder":"sort_order","durationMin":"duration_min","enrolledAt":"enrolled_at","dueDate":"due_date","maxScore":"max_score","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","remindAt":"remind_at","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin","instructor"],"defaultValue":"user"}},"Courses":{"id":{"required":true},"title":{"required":true}},"Chapters":{"id":{"required":true},"title":{"required":true}},"Students":{"id":{"required":true},"name":{"required":true}},"Assignments":{"id":{"required":true},"title":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO๏ผˆๆ–‡ไปถๅญ˜ๅ‚จ๏ผ‰","deployment":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","considerations":[]},"architectureDiagram":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ EduPlatform โ€” System Architecture โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Client Layer โ”‚ โ”‚ Admin / Web โ”‚ โ”‚\nโ”‚ โ”‚ React Native 0.76 + Expo / Flutter 3.x โ”‚ โ”‚ React + Vite (Web) โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ API Gateway โ”‚ โ”‚\nโ”‚ โ”‚ Nginx / Kong โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Backend Services โ”‚ โ”‚\nโ”‚ โ”‚ NestJS + Prisma โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ PostgreSQL 15โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ MinIO โ”‚ โ”‚\nโ”‚ โ”‚ Primary โ”‚ โ”‚ Session โ”‚ โ”‚ Files โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ Modules: โ”‚\nโ”‚ 1 ่ฏพ็จ‹ๅ‘ๅธƒ โ”‚\nโ”‚ 2 ็ซ ่Š‚็ฎก็† โ”‚\nโ”‚ 3 ๅญฆๅ‘˜่ฟ›ๅบฆ โ”‚\nโ”‚ 4 ไฝœไธšๆ‰นๆ”น โ”‚\nโ”‚ 5 ่ฏพ็จ‹็ฎก็† โ”‚\nโ”‚ 6 ้ข˜ๅบ“็ฎก็† โ”‚\nโ”‚ โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","dataFlows":[{"page":"่ฏพ็จ‹ๅ‘ๅธƒ","route":"/courses","description":"่ฏพ็จ‹ๅ‘ๅธƒ","dataFlow":["GET /api/courses โ†’ ่Žทๅ–่ฏพ็จ‹ๅ‘ๅธƒๅˆ—่กจ","POST /api/courses โ†’ ๅˆ›ๅปบ่ฏพ็จ‹ๅ‘ๅธƒ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"็ซ ่Š‚็ฎก็†","route":"/chapters","description":"็ซ ่Š‚็ฎก็†","dataFlow":["GET /api/chapters โ†’ ่Žทๅ–็ซ ่Š‚็ฎก็†ๅˆ—่กจ","POST /api/chapters โ†’ ๅˆ›ๅปบ็ซ ่Š‚็ฎก็†"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๅญฆๅ‘˜่ฟ›ๅบฆ","route":"/students","description":"ๅญฆๅ‘˜่ฟ›ๅบฆ","dataFlow":["GET /api/students โ†’ ่Žทๅ–ๅญฆๅ‘˜่ฟ›ๅบฆๅˆ—่กจ","POST /api/students โ†’ ๅˆ›ๅปบๅญฆๅ‘˜่ฟ›ๅบฆ"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ไฝœไธšๆ‰นๆ”น","route":"/assignments","description":"ไฝœไธšๆ‰นๆ”น","dataFlow":["GET /api/assignments โ†’ ่Žทๅ–ไฝœไธšๆ‰นๆ”นๅˆ—่กจ","POST /api/assignments โ†’ ๅˆ›ๅปบไฝœไธšๆ‰นๆ”น"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"}],"modules":[{"name":"่ฏพ็จ‹ๅ‘ๅธƒ","label":"่ฏพ็จ‹ๅ‘ๅธƒ","features":["่ฏพ็จ‹ๅ‘ๅธƒ"],"responsibilities":["ๆไพ› ่ฏพ็จ‹ๅ‘ๅธƒ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"็ซ ่Š‚็ฎก็†","label":"็ซ ่Š‚็ฎก็†","features":["็ซ ่Š‚็ฎก็†"],"responsibilities":["ๆไพ› ็ซ ่Š‚็ฎก็† ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ๅญฆๅ‘˜่ฟ›ๅบฆ","label":"ๅญฆๅ‘˜่ฟ›ๅบฆ","features":["ๅญฆๅ‘˜่ฟ›ๅบฆ"],"responsibilities":["ๆไพ› ๅญฆๅ‘˜่ฟ›ๅบฆ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"ไฝœไธšๆ‰นๆ”น","label":"ไฝœไธšๆ‰นๆ”น","features":["ไฝœไธšๆ‰นๆ”น"],"responsibilities":["ๆไพ› ไฝœไธšๆ‰นๆ”น ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"course","label":"่ฏพ็จ‹็ฎก็†","features":["่ฏพ็จ‹ไธญๅฟƒ","่ง†้ข‘ๆ’ญๆ”พ"],"responsibilities":["ๆไพ› ่ฏพ็จ‹ไธญๅฟƒ ็›ธๅ…ณๅŠŸ่ƒฝ","ๆไพ› ่ง†้ข‘ๆ’ญๆ”พ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"exercise","label":"้ข˜ๅบ“็ฎก็†","features":["้ข˜ๅบ“็ปƒไน "],"responsibilities":["ๆไพ› ้ข˜ๅบ“็ปƒไน  ็›ธๅ…ณๅŠŸ่ƒฝ"]}],"databaseSchema":[{"table":"users","description":"็”จๆˆท่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"courses","description":"่ฏพ็จ‹ๅ‘ๅธƒ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"instructor","type":"VARCHAR(64)"},{"name":"price","type":"DECIMAL(10,2)"},{"name":"cover_url","type":"TEXT"},{"name":"category","type":"VARCHAR(64)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_courses_user ON courses(user_id)"]},{"table":"chapters","description":"็ซ ่Š‚็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"course_id","type":"UUID","constraints":"FK โ†’ courses.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"sort_order","type":"INTEGER","constraints":"DEFAULT 0"},{"name":"duration_min","type":"INTEGER"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_chapters_user ON chapters(user_id)"]},{"table":"students","description":"ๅญฆๅ‘˜่ฟ›ๅบฆ่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(64)","constraints":"NOT NULL"},{"name":"email","type":"VARCHAR(256)"},{"name":"enrolled_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"progress","type":"DECIMAL(5,2)","constraints":"DEFAULT 0"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_students_user ON students(user_id)"]},{"table":"assignments","description":"ไฝœไธšๆ‰นๆ”น่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"course_id","type":"UUID","constraints":"FK โ†’ courses.id"},{"name":"title","type":"VARCHAR(256)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"due_date","type":"DATE"},{"name":"max_score","type":"INTEGER","constraints":"DEFAULT 100"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_assignments_user ON assignments(user_id)"]}],"apiDesign":[{"resource":"assignments","basePath":"/api/assignments","endpoints":[{"method":"GET","path":"/api/assignments","description":"่Žทๅ–ไฝœไธšๆ‰นๆ”นๅˆ—่กจ"},{"method":"POST","path":"/api/assignments","description":"ๅˆ›ๅปบไฝœไธšๆ‰นๆ”น"}]},{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}]},{"resource":"chapters","basePath":"/api/chapters","endpoints":[{"method":"GET","path":"/api/chapters","description":"่Žทๅ–็ซ ่Š‚็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/chapters","description":"ๅˆ›ๅปบ็ซ ่Š‚็ฎก็†"}]},{"resource":"courses","basePath":"/api/courses","endpoints":[{"method":"GET","path":"/api/courses","description":"่Žทๅ–่ฏพ็จ‹ๅ‘ๅธƒๅˆ—่กจ"},{"method":"POST","path":"/api/courses","description":"ๅˆ›ๅปบ่ฏพ็จ‹ๅ‘ๅธƒ"}]},{"resource":"students","basePath":"/api/students","endpoints":[{"method":"GET","path":"/api/students","description":"่Žทๅ–ๅญฆๅ‘˜่ฟ›ๅบฆๅˆ—่กจ"},{"method":"POST","path":"/api/students","description":"ๅˆ›ๅปบๅญฆๅ‘˜่ฟ›ๅบฆ"}]}],"directoryStructure":["eduplatform/","โ”œโ”€โ”€ apps/","โ”‚ โ”œโ”€โ”€ mobile/ # React Native / Flutter ็งปๅŠจ็ซฏ","โ”‚ โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # ้กต้ข็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ฏพ็จ‹ๅ‘ๅธƒ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ็ซ ่Š‚็ฎก็†/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅญฆๅ‘˜่ฟ›ๅบฆ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ไฝœไธšๆ‰นๆ”น/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ้€š็”จ็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # ่‡ชๅฎšไน‰ Hooks","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API ่ฐƒ็”จๅฑ‚","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ # ็Šถๆ€็ฎก็†","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ","โ”‚ โ”‚ โ”œโ”€โ”€ app.json","โ”‚ โ”‚ โ””โ”€โ”€ package.json","โ”‚ โ””โ”€โ”€ web/ # Web ็ฎก็†ๅŽๅฐ","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ pages/","โ”‚ โ”‚ โ”œโ”€โ”€ components/","โ”‚ โ”‚ โ””โ”€โ”€ layouts/","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ packages/","โ”‚ โ””โ”€โ”€ shared/ # ๅ…ฑไบซ็ฑปๅž‹/ๅธธ้‡","โ”œโ”€โ”€ server/ # NestJS ๅŽ็ซฏๆœๅŠก","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ modules/ # ไธšๅŠกๆจกๅ—","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ฏพ็จ‹ๅ‘ๅธƒ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ฏพ็จ‹ๅ‘ๅธƒ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ฏพ็จ‹ๅ‘ๅธƒ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ฏพ็จ‹ๅ‘ๅธƒ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ็ซ ่Š‚็ฎก็†/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ็ซ ่Š‚็ฎก็†.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ็ซ ่Š‚็ฎก็†.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ็ซ ่Š‚็ฎก็†.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅญฆๅ‘˜่ฟ›ๅบฆ/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅญฆๅ‘˜่ฟ›ๅบฆ.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅญฆๅ‘˜่ฟ›ๅบฆ.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅญฆๅ‘˜่ฟ›ๅบฆ.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ไฝœไธšๆ‰นๆ”น/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ไฝœไธšๆ‰นๆ”น.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ไฝœไธšๆ‰นๆ”น.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ไฝœไธšๆ‰นๆ”น.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ course/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ course.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ course.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ course.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”œโ”€โ”€ common/ # ้€š็”จ๏ผˆguards, filters, interceptors๏ผ‰","โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ # Prisma ORM","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ schema.prisma","โ”‚ โ”‚ โ”œโ”€โ”€ config/ # ็Žฏๅขƒ้…็ฝฎ","โ”‚ โ”‚ โ””โ”€โ”€ main.ts","โ”‚ โ”œโ”€โ”€ test/","โ”‚ โ”œโ”€โ”€ Dockerfile","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ docker-compose.yml","โ”œโ”€โ”€ .github/workflows/ # CI/CD","โ”‚ โ””โ”€โ”€ ci.yml","โ”œโ”€โ”€ .env.example","โ”œโ”€โ”€ README.md","โ””โ”€โ”€ turbo.json # Monorepo ้…็ฝฎ"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions โ†’ Build โ†’ Test โ†’ Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:37.455Z","sourceDomain":"education","moduleCount":6,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/course/backend/.gitignore b/.benchmark/course/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/course/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/course/backend/README.md b/.benchmark/course/backend/README.md new file mode 100644 index 0000000..879ffbb --- /dev/null +++ b/.benchmark/course/backend/README.md @@ -0,0 +1,90 @@ +# EduPlatform โ€” Backend API + +> ไธ€ไธชๆ”ฏๆŒๅœจ็บฟ่ฏพ็จ‹ๅญฆไน ใ€็›ดๆ’ญๆŽˆ่ฏพใ€้ข˜ๅบ“็ปƒไน ๅ’Œๅญฆๆƒ…่ฟฝ่ธช็š„ๆ•™่‚ฒๅนณๅฐใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### courses +- `GET /api/courses` โ€” List all +- `GET /api/courses/:id` โ€” Get by ID +- `POST /api/courses` โ€” Create +- `PUT /api/courses/:id` โ€” Update +- `DELETE /api/courses/:id` โ€” Delete + +#### chapters +- `GET /api/chapters` โ€” List all +- `GET /api/chapters/:id` โ€” Get by ID +- `POST /api/chapters` โ€” Create +- `PUT /api/chapters/:id` โ€” Update +- `DELETE /api/chapters/:id` โ€” Delete + +#### students +- `GET /api/students` โ€” List all +- `GET /api/students/:id` โ€” Get by ID +- `POST /api/students` โ€” Create +- `PUT /api/students/:id` โ€” Update +- `DELETE /api/students/:id` โ€” Delete + +#### assignments +- `GET /api/assignments` โ€” List all +- `GET /api/assignments/:id` โ€” Get by ID +- `POST /api/assignments` โ€” Create +- `PUT /api/assignments/:id` โ€” Update +- `DELETE /api/assignments/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/course/backend/package.json b/.benchmark/course/backend/package.json new file mode 100644 index 0000000..1493a8d --- /dev/null +++ b/.benchmark/course/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/backend/src/__tests__/assignments.test.ts b/.benchmark/course/backend/src/__tests__/assignments.test.ts new file mode 100644 index 0000000..3f49cfe --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/assignments.test.ts @@ -0,0 +1,120 @@ +// Assignments CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "courseId": "sample-courseid", + "title": "sample-title", + "description": "sample-description", + "dueDate": "sample-duedate" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/assignments", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/assignments", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/assignments", () => { + it("creates a assignment", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/assignments", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/assignments/:id", () => { + it("returns the created assignment", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assignments/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/assignments/:id", () => { + it("updates the assignment", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/assignments/:id", () => { + it("deletes the assignment", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/assignments/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/auth.test.ts b/.benchmark/course/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/chapters.test.ts b/.benchmark/course/backend/src/__tests__/chapters.test.ts new file mode 100644 index 0000000..e99c835 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/chapters.test.ts @@ -0,0 +1,119 @@ +// Chapters CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "courseId": "sample-courseid", + "title": "sample-title", + "durationMin": 1 + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/chapters", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/chapters", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/chapters", () => { + it("creates a chapter", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/chapters", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/chapters/:id", () => { + it("returns the created chapter", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/chapters/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/chapters/:id", () => { + it("updates the chapter", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/chapters/:id", () => { + it("deletes the chapter", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/chapters/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/courses.test.ts b/.benchmark/course/backend/src/__tests__/courses.test.ts new file mode 100644 index 0000000..5ff6b9f --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/courses.test.ts @@ -0,0 +1,122 @@ +// Courses CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "instructor": "sample-instructor", + "price": 1, + "coverUrl": "sample-coverurl", + "category": "sample-category" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/courses", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/courses", () => { + it("creates a cours", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/courses/:id", () => { + it("returns the created cours", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/courses/:id", () => { + it("updates the cours", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/courses/:id", () => { + it("deletes the cours", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/exercises.test.ts b/.benchmark/course/backend/src/__tests__/exercises.test.ts new file mode 100644 index 0000000..3845bdf --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/exercises.test.ts @@ -0,0 +1,120 @@ +// Exercis CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/exercises", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/exercises", () => { + it("creates a exercis", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/exercises/:id", () => { + it("returns the created exercis", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/exercises/:id", () => { + it("updates the exercis", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/exercises/:id", () => { + it("deletes the exercis", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/lessons.test.ts b/.benchmark/course/backend/src/__tests__/lessons.test.ts new file mode 100644 index 0000000..adcd9d7 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/lessons.test.ts @@ -0,0 +1,122 @@ +// Lesson CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/lessons", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/lessons", () => { + it("creates a lesson", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/lessons/:id", () => { + it("returns the created lesson", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/lessons/:id", () => { + it("updates the lesson", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/lessons/:id", () => { + it("deletes the lesson", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/students.test.ts b/.benchmark/course/backend/src/__tests__/students.test.ts new file mode 100644 index 0000000..945cab6 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/students.test.ts @@ -0,0 +1,118 @@ +// Students CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/students", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/students", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/students", () => { + it("creates a student", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/students", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/students/:id", () => { + it("returns the created student", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/students/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/students/:id", () => { + it("updates the student", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/students/:id", () => { + it("deletes the student", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/students/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/user_progress.test.ts b/.benchmark/course/backend/src/__tests__/user_progress.test.ts new file mode 100644 index 0000000..86811c6 --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/user_progress.test.ts @@ -0,0 +1,122 @@ +// UserProgress CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/user_progress", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/user_progress", () => { + it("creates a user_progress", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/user_progress/:id", () => { + it("returns the created user_progress", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/user_progress/:id", () => { + it("updates the user_progress", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/user_progress/:id", () => { + it("deletes the user_progress", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/__tests__/users.test.ts b/.benchmark/course/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/course/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/backend/src/db/client.ts b/.benchmark/course/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/course/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/course/backend/src/db/schema.ts b/.benchmark/course/backend/src/db/schema.ts new file mode 100644 index 0000000..57e05a7 --- /dev/null +++ b/.benchmark/course/backend/src/db/schema.ts @@ -0,0 +1,16 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS courses (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n instructor TEXT,\n price REAL,\n cover_url TEXT,\n category TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS chapters (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n sort_order INTEGER,\n duration_min INTEGER,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS students (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n enrolled_at TEXT,\n progress REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assignments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n description TEXT,\n due_date TEXT,\n max_score INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/course/backend/src/index.ts b/.benchmark/course/backend/src/index.ts new file mode 100644 index 0000000..f8dda5f --- /dev/null +++ b/.benchmark/course/backend/src/index.ts @@ -0,0 +1,71 @@ +// EduPlatform โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { coursesRoutes } from "./routes/courses.js"; +import { chaptersRoutes } from "./routes/chapters.js"; +import { studentsRoutes } from "./routes/students.js"; +import { assignmentsRoutes } from "./routes/assignments.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-c08d8e17"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(coursesRoutes, { prefix: "/api/courses" }); + await app.register(chaptersRoutes, { prefix: "/api/chapters" }); + await app.register(studentsRoutes, { prefix: "/api/students" }); + await app.register(assignmentsRoutes, { prefix: "/api/assignments" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/course/backend/src/middleware/auth.ts b/.benchmark/course/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/course/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/course/backend/src/routes/assignments.ts b/.benchmark/course/backend/src/routes/assignments.ts new file mode 100644 index 0000000..155cf1d --- /dev/null +++ b/.benchmark/course/backend/src/routes/assignments.ts @@ -0,0 +1,51 @@ +// Auto-generated Assignments routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { AssignmentsService } from "../services/assignment.js"; +import type { CreateAssignmentsInput, UpdateAssignmentsInput } from "../types/index.js"; + +const service = new AssignmentsService(); + +export async function assignmentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/assignments โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/assignments/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assignments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/assignments โ€” create + app.post<{ Body: CreateAssignmentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/assignments/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateAssignmentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Assignments not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/assignments/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Assignments not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/auth.ts b/.benchmark/course/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/course/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/course/backend/src/routes/chapters.ts b/.benchmark/course/backend/src/routes/chapters.ts new file mode 100644 index 0000000..0f8bac6 --- /dev/null +++ b/.benchmark/course/backend/src/routes/chapters.ts @@ -0,0 +1,51 @@ +// Auto-generated Chapters routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ChaptersService } from "../services/chapter.js"; +import type { CreateChaptersInput, UpdateChaptersInput } from "../types/index.js"; + +const service = new ChaptersService(); + +export async function chaptersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/chapters โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/chapters/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Chapters not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/chapters โ€” create + app.post<{ Body: CreateChaptersInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/chapters/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateChaptersInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Chapters not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/chapters/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Chapters not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/courses.ts b/.benchmark/course/backend/src/routes/courses.ts new file mode 100644 index 0000000..4ec97c0 --- /dev/null +++ b/.benchmark/course/backend/src/routes/courses.ts @@ -0,0 +1,51 @@ +// Auto-generated Courses routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CoursesService } from "../services/cours.js"; +import type { CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +const service = new CoursesService(); + +export async function coursesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/courses โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/courses/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/courses โ€” create + app.post<{ Body: CreateCoursesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/courses/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCoursesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/courses/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/exercises.ts b/.benchmark/course/backend/src/routes/exercises.ts new file mode 100644 index 0000000..352bcb9 --- /dev/null +++ b/.benchmark/course/backend/src/routes/exercises.ts @@ -0,0 +1,51 @@ +// Auto-generated Exercis routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ExercisService } from "../services/exercis.js"; +import type { CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +const service = new ExercisService(); + +export async function exercisesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/exercises โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/exercises/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/exercises โ€” create + app.post<{ Body: CreateExercisInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/exercises/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateExercisInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/exercises/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/lessons.ts b/.benchmark/course/backend/src/routes/lessons.ts new file mode 100644 index 0000000..22468bd --- /dev/null +++ b/.benchmark/course/backend/src/routes/lessons.ts @@ -0,0 +1,51 @@ +// Auto-generated Lesson routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { LessonService } from "../services/lesson.js"; +import type { CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +const service = new LessonService(); + +export async function lessonsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/lessons โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/lessons/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/lessons โ€” create + app.post<{ Body: CreateLessonInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/lessons/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateLessonInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/lessons/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/students.ts b/.benchmark/course/backend/src/routes/students.ts new file mode 100644 index 0000000..6b97d76 --- /dev/null +++ b/.benchmark/course/backend/src/routes/students.ts @@ -0,0 +1,51 @@ +// Auto-generated Students routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { StudentsService } from "../services/student.js"; +import type { CreateStudentsInput, UpdateStudentsInput } from "../types/index.js"; + +const service = new StudentsService(); + +export async function studentsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/students โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/students/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Students not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/students โ€” create + app.post<{ Body: CreateStudentsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/students/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateStudentsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Students not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/students/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Students not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/user_progress.ts b/.benchmark/course/backend/src/routes/user_progress.ts new file mode 100644 index 0000000..ac9fec6 --- /dev/null +++ b/.benchmark/course/backend/src/routes/user_progress.ts @@ -0,0 +1,51 @@ +// Auto-generated UserProgress routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserProgressService } from "../services/user_progress.js"; +import type { CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +const service = new UserProgressService(); + +export async function user_progressRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/user_progress โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/user_progress/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/user_progress โ€” create + app.post<{ Body: CreateUserProgressInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/user_progress/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserProgressInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/user_progress/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/routes/users.ts b/.benchmark/course/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/course/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/backend/src/services/assignment.ts b/.benchmark/course/backend/src/services/assignment.ts new file mode 100644 index 0000000..4fbdee4 --- /dev/null +++ b/.benchmark/course/backend/src/services/assignment.ts @@ -0,0 +1,55 @@ +// Auto-generated Assignments service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Assignments, CreateAssignmentsInput, UpdateAssignmentsInput } from "../types/index.js"; + +export class AssignmentsService { + /** List all assignments */ + list(): Assignments[] { + return queryAll("SELECT * FROM assignments ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Assignments | undefined { + return queryOne("SELECT * FROM assignments WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateAssignmentsInput): Assignments { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "course_id", "title", "description", "due_date", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.courseId ?? null, input.title ?? null, input.description ?? null, input.dueDate ?? null, now, now]; + + execute(`INSERT INTO assignments (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateAssignmentsInput): Assignments | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.dueDate !== undefined) { sets.push("due_date = ?"); values.push(input.dueDate); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE assignments SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM assignments WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/chapter.ts b/.benchmark/course/backend/src/services/chapter.ts new file mode 100644 index 0000000..cb79b82 --- /dev/null +++ b/.benchmark/course/backend/src/services/chapter.ts @@ -0,0 +1,54 @@ +// Auto-generated Chapters service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Chapters, CreateChaptersInput, UpdateChaptersInput } from "../types/index.js"; + +export class ChaptersService { + /** List all chapters */ + list(): Chapters[] { + return queryAll("SELECT * FROM chapters ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Chapters | undefined { + return queryOne("SELECT * FROM chapters WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateChaptersInput): Chapters { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "course_id", "title", "duration_min", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.courseId ?? null, input.title ?? null, input.durationMin ?? null, now, now]; + + execute(`INSERT INTO chapters (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateChaptersInput): Chapters | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.durationMin !== undefined) { sets.push("duration_min = ?"); values.push(input.durationMin); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE chapters SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM chapters WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/cours.ts b/.benchmark/course/backend/src/services/cours.ts new file mode 100644 index 0000000..38e9af2 --- /dev/null +++ b/.benchmark/course/backend/src/services/cours.ts @@ -0,0 +1,57 @@ +// Auto-generated Courses service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Courses, CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +export class CoursesService { + /** List all courses */ + list(): Courses[] { + return queryAll("SELECT * FROM courses ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Courses | undefined { + return queryOne("SELECT * FROM courses WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCoursesInput): Courses { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "instructor", "price", "cover_url", "category", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.instructor ?? null, input.price ?? null, input.coverUrl ?? null, input.category ?? null, now, now]; + + execute(`INSERT INTO courses (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCoursesInput): Courses | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.instructor !== undefined) { sets.push("instructor = ?"); values.push(input.instructor); } + if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); } + if (input.coverUrl !== undefined) { sets.push("cover_url = ?"); values.push(input.coverUrl); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE courses SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM courses WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/exercis.ts b/.benchmark/course/backend/src/services/exercis.ts new file mode 100644 index 0000000..c6bef85 --- /dev/null +++ b/.benchmark/course/backend/src/services/exercis.ts @@ -0,0 +1,56 @@ +// Auto-generated Exercis service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Exercis, CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +export class ExercisService { + /** List all exercises */ + list(): Exercis[] { + return queryAll("SELECT * FROM exercises ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Exercis | undefined { + return queryOne("SELECT * FROM exercises WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateExercisInput): Exercis { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "lesson_id", "question", "options", "answer"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.lessonId ?? null, input.question ?? null, input.options ?? null, input.answer ?? null]; + + execute(`INSERT INTO exercises (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateExercisInput): Exercis | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.question !== undefined) { sets.push("question = ?"); values.push(input.question); } + if (input.options !== undefined) { sets.push("options = ?"); values.push(input.options); } + if (input.answer !== undefined) { sets.push("answer = ?"); values.push(input.answer); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE exercises SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM exercises WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/lesson.ts b/.benchmark/course/backend/src/services/lesson.ts new file mode 100644 index 0000000..2ddf9ab --- /dev/null +++ b/.benchmark/course/backend/src/services/lesson.ts @@ -0,0 +1,57 @@ +// Auto-generated Lesson service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Lesson, CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +export class LessonService { + /** List all lessons */ + list(): Lesson[] { + return queryAll("SELECT * FROM lessons ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Lesson | undefined { + return queryOne("SELECT * FROM lessons WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateLessonInput): Lesson { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "course_id", "title", "video_url", "duration_sec", "sort_order"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.courseId ?? null, input.title ?? null, input.videoUrl ?? null, input.durationSec ?? null, input.sortOrder ?? null]; + + execute(`INSERT INTO lessons (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateLessonInput): Lesson | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.videoUrl !== undefined) { sets.push("video_url = ?"); values.push(input.videoUrl); } + if (input.durationSec !== undefined) { sets.push("duration_sec = ?"); values.push(input.durationSec); } + if (input.sortOrder !== undefined) { sets.push("sort_order = ?"); values.push(input.sortOrder); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE lessons SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM lessons WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/student.ts b/.benchmark/course/backend/src/services/student.ts new file mode 100644 index 0000000..595ecaa --- /dev/null +++ b/.benchmark/course/backend/src/services/student.ts @@ -0,0 +1,53 @@ +// Auto-generated Students service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Students, CreateStudentsInput, UpdateStudentsInput } from "../types/index.js"; + +export class StudentsService { + /** List all students */ + list(): Students[] { + return queryAll("SELECT * FROM students ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Students | undefined { + return queryOne("SELECT * FROM students WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateStudentsInput): Students { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, now, now]; + + execute(`INSERT INTO students (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateStudentsInput): Students | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE students SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM students WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/user.ts b/.benchmark/course/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/course/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/services/user_progress.ts b/.benchmark/course/backend/src/services/user_progress.ts new file mode 100644 index 0000000..a14a7c8 --- /dev/null +++ b/.benchmark/course/backend/src/services/user_progress.ts @@ -0,0 +1,56 @@ +// Auto-generated UserProgress service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { UserProgress, CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +export class UserProgressService { + /** List all user_progress */ + list(): UserProgress[] { + return queryAll("SELECT * FROM user_progress ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): UserProgress | undefined { + return queryOne("SELECT * FROM user_progress WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserProgressInput): UserProgress { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "lesson_id", "completed", "watched_sec"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.lessonId ?? null, input.completed ?? null, input.watchedSec ?? null]; + + execute(`INSERT INTO user_progress (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserProgressInput): UserProgress | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.completed !== undefined) { sets.push("completed = ?"); values.push(input.completed); } + if (input.watchedSec !== undefined) { sets.push("watched_sec = ?"); values.push(input.watchedSec); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE user_progress SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM user_progress WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/backend/src/types/fastify.d.ts b/.benchmark/course/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/course/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/course/backend/src/types/index.ts b/.benchmark/course/backend/src/types/index.ts new file mode 100644 index 0000000..0bb3219 --- /dev/null +++ b/.benchmark/course/backend/src/types/index.ts @@ -0,0 +1,185 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateCoursesInput { + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface CreateChaptersInput { + userId?: string; + courseId?: string; + title: string; + durationMin?: number; +} + +export interface CreateStudentsInput { + userId?: string; + name: string; + email?: string; +} + +export interface CreateAssignmentsInput { + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateCoursesInput { + userId?: string; + title?: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface UpdateChaptersInput { + userId?: string; + courseId?: string; + title?: string; + durationMin?: number; +} + +export interface UpdateStudentsInput { + userId?: string; + name?: string; + email?: string; +} + +export interface UpdateAssignmentsInput { + userId?: string; + courseId?: string; + title?: string; + description?: string; + dueDate?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/course/backend/src/types/sql.js.d.ts b/.benchmark/course/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/course/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/course/backend/tsconfig.json b/.benchmark/course/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/course/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/course/electron/README.md b/.benchmark/course/electron/README.md new file mode 100644 index 0000000..971613c --- /dev/null +++ b/.benchmark/course/electron/README.md @@ -0,0 +1,82 @@ +# eduplatform โ€” Desktop + +> Electron desktop application wrapping the eduplatform fullstack web project. + +## Architecture + +``` +Web Fullstack Project + ร— + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** โ€” Window management, menu, tray, IPC, auto-update +- **Preload** โ€” Secure bridge between main and renderer +- **Renderer** โ€” Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +โ”œโ”€โ”€ main.ts โ€” Main process (BrowserWindow, Menu, lifecycle) +โ”œโ”€โ”€ preload.ts โ€” Secure IPC bridge (contextBridge) +โ”œโ”€โ”€ ipc.ts โ€” IPC handlers (dialogs, store, notifications) +โ”œโ”€โ”€ tray.ts โ€” System tray +โ”œโ”€โ”€ updater.ts โ€” Auto-update (electron-updater) +โ””โ”€โ”€ utils.ts โ€” Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/course/electron/assets/icon.png b/.benchmark/course/electron/assets/icon.png new file mode 100644 index 0000000..554e8a3 Binary files /dev/null and b/.benchmark/course/electron/assets/icon.png differ diff --git a/.benchmark/course/electron/electron-builder.yml b/.benchmark/course/electron/electron-builder.yml new file mode 100644 index 0000000..806e64c --- /dev/null +++ b/.benchmark/course/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml โ€” Auto-generated by SF-06 + +appId: com.eduplatform.desktop +productName: eduplatform +copyright: "Copyright ยฉ 2026 eduplatform" + +# โ”€โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +directories: + output: release + buildResources: assets + +# โ”€โ”€โ”€ Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# โ”€โ”€โ”€ Extra Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# โ”€โ”€โ”€ macOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# โ”€โ”€โ”€ Windows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: eduplatform + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: eduplatform + +# โ”€โ”€โ”€ Linux โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: eduplatform + Comment: eduplatform Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# โ”€โ”€โ”€ Auto Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +publish: + provider: generic + url: https://releases.example.com/eduplatform/ + channel: latest diff --git a/.benchmark/course/electron/electron/ipc.ts b/.benchmark/course/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/course/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // โ”€โ”€ App Info โ”€โ”€ + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // โ”€โ”€ Window Controls โ”€โ”€ + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // โ”€โ”€ File Dialogs โ”€โ”€ + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // โ”€โ”€ Notifications โ”€โ”€ + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // โ”€โ”€ Shell โ”€โ”€ + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // โ”€โ”€ Persistent Store โ”€โ”€ + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/course/electron/electron/main.ts b/.benchmark/course/electron/electron/main.ts new file mode 100644 index 0000000..6cd3381 --- /dev/null +++ b/.benchmark/course/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "eduplatform"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// โ”€โ”€โ”€ Create Window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // โ”€โ”€ Load content โ”€โ”€ + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // โ”€โ”€ Show when ready โ”€โ”€ + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // โ”€โ”€ External links โ†’ default browser โ”€โ”€ + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // โ”€โ”€ Prevent navigation away โ”€โ”€ + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// โ”€โ”€โ”€ Icon Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// โ”€โ”€โ”€ API Server (Production) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// โ”€โ”€โ”€ Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// โ”€โ”€โ”€ App Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed โ€” continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/course/electron/electron/preload.ts b/.benchmark/course/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/course/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// โ”€โ”€โ”€ Expose safe API to renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +contextBridge.exposeInMainWorld("electronAPI", { + // โ”€โ”€ App Info โ”€โ”€ + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // โ”€โ”€ Window Controls โ”€โ”€ + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // โ”€โ”€ File Dialogs โ”€โ”€ + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // โ”€โ”€ Notifications โ”€โ”€ + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // โ”€โ”€ Shell โ”€โ”€ + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // โ”€โ”€ Store โ”€โ”€ + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // โ”€โ”€ Updates โ”€โ”€ + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // โ”€โ”€ Update Events โ”€โ”€ + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // โ”€โ”€ Remove listener โ”€โ”€ + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// โ”€โ”€โ”€ Type declarations for renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/course/electron/electron/tray.ts b/.benchmark/course/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/course/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/course/electron/electron/updater.ts b/.benchmark/course/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/course/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/course/electron/electron/utils.ts b/.benchmark/course/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/course/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// โ”€โ”€โ”€ Dev mode detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function isDev(): boolean { + return !app.isPackaged; +} + +// โ”€โ”€โ”€ Web URL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// โ”€โ”€โ”€ API Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// โ”€โ”€โ”€ Get API port from env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/course/electron/entitlements.mac.plist b/.benchmark/course/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/course/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/course/electron/package.json b/.benchmark/course/electron/package.json new file mode 100644 index 0000000..d8bbeee --- /dev/null +++ b/.benchmark/course/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "eduplatform-desktop", + "version": "1.0.0", + "private": true, + "description": "eduplatform โ€” Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "eduplatform", + "appId": "com.eduplatform.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/course/electron/tsconfig.json b/.benchmark/course/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/course/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/assignments/page.tsx b/.benchmark/course/frontend/app/assignments/page.tsx new file mode 100644 index 0000000..ed98102 --- /dev/null +++ b/.benchmark/course/frontend/app/assignments/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useAssignments } from "@/hooks/useAssignments"; + +export default function PagePage() { + const { data, loading, error, refetch } = useAssignments(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ไฝœไธšๆ‰นๆ”น

+

ไฝœไธšๆ‰นๆ”น

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/chapters/page.tsx b/.benchmark/course/frontend/app/chapters/page.tsx new file mode 100644 index 0000000..4d55e33 --- /dev/null +++ b/.benchmark/course/frontend/app/chapters/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useChapters } from "@/hooks/useChapters"; + +export default function PagePage() { + const { data, loading, error, refetch } = useChapters(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

็ซ ่Š‚็ฎก็†

+

็ซ ่Š‚็ฎก็†

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/course/[id]/page.tsx b/.benchmark/course/frontend/app/course/[id]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/frontend/app/course/[id]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 ๅฎŒๅ…จๆŒ‡ๅ—", instructor: "ๅผ ่€ๅธˆ", price: 199, cover: "๐Ÿ“˜" }, + { id: "c2", title: "TypeScript ้ซ˜็บง็ผ–็จ‹", instructor: "ๆŽ่€ๅธˆ", price: 149, cover: "๐Ÿ“™" }, + { id: "c3", title: "Node.js ๅŽ็ซฏๅฎžๆˆ˜", instructor: "็Ž‹่€ๅธˆ", price: 249, cover: "๐Ÿ“—" }, +]; + +export default function CoursePage() { + return ( +
+

่ฏพ็จ‹ไธญๅฟƒ

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

ยฅ{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/courses/page.tsx b/.benchmark/course/frontend/app/courses/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/frontend/app/courses/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 ๅฎŒๅ…จๆŒ‡ๅ—", instructor: "ๅผ ่€ๅธˆ", price: 199, cover: "๐Ÿ“˜" }, + { id: "c2", title: "TypeScript ้ซ˜็บง็ผ–็จ‹", instructor: "ๆŽ่€ๅธˆ", price: 149, cover: "๐Ÿ“™" }, + { id: "c3", title: "Node.js ๅŽ็ซฏๅฎžๆˆ˜", instructor: "็Ž‹่€ๅธˆ", price: 249, cover: "๐Ÿ“—" }, +]; + +export default function CoursePage() { + return ( +
+

่ฏพ็จ‹ไธญๅฟƒ

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

ยฅ{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/exercises/page.tsx b/.benchmark/course/frontend/app/exercises/page.tsx new file mode 100644 index 0000000..15d71e4 --- /dev/null +++ b/.benchmark/course/frontend/app/exercises/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function ExercisePage() { + return ( +
+

้ข˜ๅบ“็ปƒไน 

+

็ซ ่Š‚็ปƒไน ๅ’Œๆจกๆ‹Ÿ่€ƒ่ฏ•ๅŠŸ่ƒฝ

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/globals.css b/.benchmark/course/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/course/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/course/frontend/app/layout.tsx b/.benchmark/course/frontend/app/layout.tsx new file mode 100644 index 0000000..8be9dca --- /dev/null +++ b/.benchmark/course/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "EduPlatform", + description: "ไธ€ไธชๆ”ฏๆŒๅœจ็บฟ่ฏพ็จ‹ๅญฆไน ใ€็›ดๆ’ญๆŽˆ่ฏพใ€้ข˜ๅบ“็ปƒไน ๅ’Œๅญฆๆƒ…่ฟฝ่ธช็š„ๆ•™่‚ฒๅนณๅฐใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx b/.benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/frontend/app/learn/[courseId]/[lessonId]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 ๅฎŒๅ…จๆŒ‡ๅ—", instructor: "ๅผ ่€ๅธˆ", price: 199, cover: "๐Ÿ“˜" }, + { id: "c2", title: "TypeScript ้ซ˜็บง็ผ–็จ‹", instructor: "ๆŽ่€ๅธˆ", price: 149, cover: "๐Ÿ“™" }, + { id: "c3", title: "Node.js ๅŽ็ซฏๅฎžๆˆ˜", instructor: "็Ž‹่€ๅธˆ", price: 249, cover: "๐Ÿ“—" }, +]; + +export default function CoursePage() { + return ( +
+

่ฏพ็จ‹ไธญๅฟƒ

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

ยฅ{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/live/page.tsx b/.benchmark/course/frontend/app/live/page.tsx new file mode 100644 index 0000000..8679f8f --- /dev/null +++ b/.benchmark/course/frontend/app/live/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function LivePage() { + return ( +
+

็›ดๆ’ญ่ฏพๅ ‚

+

่ฏพ็จ‹็›ดๆ’ญๅŠŸ่ƒฝ

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/frontend/app/page.tsx b/.benchmark/course/frontend/app/page.tsx new file mode 100644 index 0000000..47bd262 --- /dev/null +++ b/.benchmark/course/frontend/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

EduPlatform

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ่ฏพ็จ‹ๅ‘ๅธƒ + + + ๐Ÿ“… + ็ซ ่Š‚็ฎก็† + + + ๐Ÿ“ + ๅญฆๅ‘˜่ฟ›ๅบฆ + + + ๐Ÿ“ธ + ไฝœไธšๆ‰นๆ”น + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ EduPlatform

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/course/frontend/app/profile/page.tsx b/.benchmark/course/frontend/app/profile/page.tsx new file mode 100644 index 0000000..ed7c604 --- /dev/null +++ b/.benchmark/course/frontend/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆˆ‘็š„

+

ๅญฆไน ๆŠฅๅ‘Šใ€ๅทฒ่ดญ่ฏพ็จ‹ใ€่ฎพ็ฝฎ

+
+ +
+ + { +

ๆˆ‘็š„ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile

+
} +
+ ); +} diff --git a/.benchmark/course/frontend/app/students/page.tsx b/.benchmark/course/frontend/app/students/page.tsx new file mode 100644 index 0000000..4de9051 --- /dev/null +++ b/.benchmark/course/frontend/app/students/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useStudents } from "@/hooks/useStudents"; + +export default function PagePage() { + const { data, loading, error, refetch } = useStudents(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅญฆๅ‘˜่ฟ›ๅบฆ

+

ๅญฆๅ‘˜่ฟ›ๅบฆ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/ไฝœไธšๆ‰นๆ”น/page.tsx b/.benchmark/course/frontend/app/ไฝœไธšๆ‰นๆ”น/page.tsx new file mode 100644 index 0000000..f49e3d1 --- /dev/null +++ b/.benchmark/course/frontend/app/ไฝœไธšๆ‰นๆ”น/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useไฝœไธšๆ‰นๆ”น } from "@/hooks/useไฝœไธšๆ‰นๆ”น"; + +export default function PagePage() { + const { data, loading, error, refetch } = useไฝœไธšๆ‰นๆ”น(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ไฝœไธšๆ‰นๆ”น

+

ไฝœไธšๆ‰นๆ”น

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/ๅญฆๅ‘˜่ฟ›ๅบฆ/page.tsx b/.benchmark/course/frontend/app/ๅญฆๅ‘˜่ฟ›ๅบฆ/page.tsx new file mode 100644 index 0000000..b58312b --- /dev/null +++ b/.benchmark/course/frontend/app/ๅญฆๅ‘˜่ฟ›ๅบฆ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useๅญฆๅ‘˜่ฟ›ๅบฆ } from "@/hooks/useๅญฆๅ‘˜่ฟ›ๅบฆ"; + +export default function PagePage() { + const { data, loading, error, refetch } = useๅญฆๅ‘˜่ฟ›ๅบฆ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅญฆๅ‘˜่ฟ›ๅบฆ

+

ๅญฆๅ‘˜่ฟ›ๅบฆ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/็ซ ่Š‚็ฎก็†/page.tsx b/.benchmark/course/frontend/app/็ซ ่Š‚็ฎก็†/page.tsx new file mode 100644 index 0000000..1e8a8fe --- /dev/null +++ b/.benchmark/course/frontend/app/็ซ ่Š‚็ฎก็†/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use็ซ ่Š‚็ฎก็† } from "@/hooks/use็ซ ่Š‚็ฎก็†"; + +export default function PagePage() { + const { data, loading, error, refetch } = use็ซ ่Š‚็ฎก็†(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

็ซ ่Š‚็ฎก็†

+

็ซ ่Š‚็ฎก็†

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/app/่ฏพ็จ‹ๅ‘ๅธƒ/page.tsx b/.benchmark/course/frontend/app/่ฏพ็จ‹ๅ‘ๅธƒ/page.tsx new file mode 100644 index 0000000..e60d110 --- /dev/null +++ b/.benchmark/course/frontend/app/่ฏพ็จ‹ๅ‘ๅธƒ/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { use่ฏพ็จ‹ๅ‘ๅธƒ } from "@/hooks/use่ฏพ็จ‹ๅ‘ๅธƒ"; + +export default function PagePage() { + const { data, loading, error, refetch } = use่ฏพ็จ‹ๅ‘ๅธƒ(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

่ฏพ็จ‹ๅ‘ๅธƒ

+

่ฏพ็จ‹ๅ‘ๅธƒ

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/course/frontend/components/layout/BottomNav.tsx b/.benchmark/course/frontend/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/course/frontend/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/course/frontend/components/layout/Sidebar.tsx b/.benchmark/course/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/course/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/course/frontend/components/ui/Button.tsx b/.benchmark/course/frontend/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/course/frontend/components/ui/Card.tsx b/.benchmark/course/frontend/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/course/frontend/components/ui/EmptyState.tsx b/.benchmark/course/frontend/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/course/frontend/components/ui/Input.tsx b/.benchmark/course/frontend/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/course/frontend/components/ui/Modal.tsx b/.benchmark/course/frontend/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/course/frontend/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/course/frontend/hooks/useAssignments.ts b/.benchmark/course/frontend/hooks/useAssignments.ts new file mode 100644 index 0000000..f0c1577 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useAssignments.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for assignments +type Assignment = Record; + +/** + * Fetch and manage assignments data. + */ +export function useAssignments() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/assignments`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useAuth.ts b/.benchmark/course/frontend/hooks/useAuth.ts new file mode 100644 index 0000000..82d7e2f --- /dev/null +++ b/.benchmark/course/frontend/hooks/useAuth.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for auth +type Auth = Record; + +/** + * Fetch and manage auth data. + */ +export function useAuth() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/auth`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useChapters.ts b/.benchmark/course/frontend/hooks/useChapters.ts new file mode 100644 index 0000000..4dab116 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useChapters.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for chapters +type Chapter = Record; + +/** + * Fetch and manage chapters data. + */ +export function useChapters() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/chapters`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useCourses.ts b/.benchmark/course/frontend/hooks/useCourses.ts new file mode 100644 index 0000000..1f4fc4a --- /dev/null +++ b/.benchmark/course/frontend/hooks/useCourses.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for courses +type Cour = Record; + +/** + * Fetch and manage courses data. + */ +export function useCourses() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/courses`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useDebounce.ts b/.benchmark/course/frontend/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/course/frontend/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/course/frontend/hooks/useExercises.ts b/.benchmark/course/frontend/hooks/useExercises.ts new file mode 100644 index 0000000..d2c16ca --- /dev/null +++ b/.benchmark/course/frontend/hooks/useExercises.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for exercises +type Exerci = Record; + +/** + * Fetch and manage exercises data. + */ +export function useExercises() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/exercises`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useForm.ts b/.benchmark/course/frontend/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/course/frontend/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/course/frontend/hooks/useItem.ts b/.benchmark/course/frontend/hooks/useItem.ts new file mode 100644 index 0000000..06d6f48 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useItem.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for ็ซ ่Š‚็ฎก็† +type Item = Record; + +/** + * Fetch and manage ็ซ ่Š‚็ฎก็† data. + */ +export function useItem() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/็ซ ่Š‚็ฎก็†`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useLessons.ts b/.benchmark/course/frontend/hooks/useLessons.ts new file mode 100644 index 0000000..0bf3571 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useLessons.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for lessons +type Lesson = Record; + +/** + * Fetch and manage lessons data. + */ +export function useLessons() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/lessons`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useProgress.ts b/.benchmark/course/frontend/hooks/useProgress.ts new file mode 100644 index 0000000..cdcac25 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useProgress.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for progress +type Progres = Record; + +/** + * Fetch and manage progress data. + */ +export function useProgress() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/progress`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/hooks/useStudents.ts b/.benchmark/course/frontend/hooks/useStudents.ts new file mode 100644 index 0000000..f000f19 --- /dev/null +++ b/.benchmark/course/frontend/hooks/useStudents.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for students +type Student = Record; + +/** + * Fetch and manage students data. + */ +export function useStudents() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/students`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/frontend/next-env.d.ts b/.benchmark/course/frontend/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/.benchmark/course/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/.benchmark/course/frontend/next.config.ts b/.benchmark/course/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/course/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/course/frontend/package.json b/.benchmark/course/frontend/package.json new file mode 100644 index 0000000..653e74f --- /dev/null +++ b/.benchmark/course/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/frontend/postcss.config.js b/.benchmark/course/frontend/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/course/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/course/frontend/services/api.ts b/.benchmark/course/frontend/services/api.ts new file mode 100644 index 0000000..dbab5bc --- /dev/null +++ b/.benchmark/course/frontend/services/api.ts @@ -0,0 +1,47 @@ +// Auto-generated API client for EduPlatform + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/course/frontend/services/assignments.ts b/.benchmark/course/frontend/services/assignments.ts new file mode 100644 index 0000000..d3f1668 --- /dev/null +++ b/.benchmark/course/frontend/services/assignments.ts @@ -0,0 +1,10 @@ +// Auto-generated assignments service +import { api } from "./api"; + +export function get() { + return api.get("/api/assignments"); +} + +export function post(data: Record) { + return api.post("/api/assignments", data); +} diff --git a/.benchmark/course/frontend/services/auth.ts b/.benchmark/course/frontend/services/auth.ts new file mode 100644 index 0000000..1536ac6 --- /dev/null +++ b/.benchmark/course/frontend/services/auth.ts @@ -0,0 +1,10 @@ +// Auto-generated auth service +import { api } from "./api"; + +export function postlogin(data: Record) { + return api.post("/api/auth", data); +} + +export function getme() { + return api.get("/api/auth"); +} diff --git a/.benchmark/course/frontend/services/chapters.ts b/.benchmark/course/frontend/services/chapters.ts new file mode 100644 index 0000000..012b896 --- /dev/null +++ b/.benchmark/course/frontend/services/chapters.ts @@ -0,0 +1,10 @@ +// Auto-generated chapters service +import { api } from "./api"; + +export function get() { + return api.get("/api/chapters"); +} + +export function post(data: Record) { + return api.post("/api/chapters", data); +} diff --git a/.benchmark/course/frontend/services/courses.ts b/.benchmark/course/frontend/services/courses.ts new file mode 100644 index 0000000..6406396 --- /dev/null +++ b/.benchmark/course/frontend/services/courses.ts @@ -0,0 +1,10 @@ +// Auto-generated courses service +import { api } from "./api"; + +export function get() { + return api.get("/api/courses"); +} + +export function post(data: Record) { + return api.post("/api/courses", data); +} diff --git a/.benchmark/course/frontend/services/exercises.ts b/.benchmark/course/frontend/services/exercises.ts new file mode 100644 index 0000000..4bd54bd --- /dev/null +++ b/.benchmark/course/frontend/services/exercises.ts @@ -0,0 +1,10 @@ +// Auto-generated exercises service +import { api } from "./api"; + +export function get() { + return api.get("/api/exercises"); +} + +export function postsubmit(data: Record) { + return api.post("/api/exercises", data); +} diff --git a/.benchmark/course/frontend/services/lessons.ts b/.benchmark/course/frontend/services/lessons.ts new file mode 100644 index 0000000..a6e1315 --- /dev/null +++ b/.benchmark/course/frontend/services/lessons.ts @@ -0,0 +1,6 @@ +// Auto-generated lessons service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/lessons/${id}`); +} diff --git a/.benchmark/course/frontend/services/progress.ts b/.benchmark/course/frontend/services/progress.ts new file mode 100644 index 0000000..a88a74b --- /dev/null +++ b/.benchmark/course/frontend/services/progress.ts @@ -0,0 +1,6 @@ +// Auto-generated progress service +import { api } from "./api"; + +export function get() { + return api.get("/api/progress"); +} diff --git a/.benchmark/course/frontend/services/students.ts b/.benchmark/course/frontend/services/students.ts new file mode 100644 index 0000000..ebb3298 --- /dev/null +++ b/.benchmark/course/frontend/services/students.ts @@ -0,0 +1,10 @@ +// Auto-generated students service +import { api } from "./api"; + +export function get() { + return api.get("/api/students"); +} + +export function post(data: Record) { + return api.post("/api/students", data); +} diff --git a/.benchmark/course/frontend/services/ไฝœไธšๆ‰นๆ”น.ts b/.benchmark/course/frontend/services/ไฝœไธšๆ‰นๆ”น.ts new file mode 100644 index 0000000..6ab83a1 --- /dev/null +++ b/.benchmark/course/frontend/services/ไฝœไธšๆ‰นๆ”น.ts @@ -0,0 +1,10 @@ +// Auto-generated ไฝœไธšๆ‰นๆ”น service +import { api } from "./api"; + +export function get() { + return api.get("/api/ไฝœไธšๆ‰นๆ”น"); +} + +export function post(data: Record) { + return api.post("/api/ไฝœไธšๆ‰นๆ”น", data); +} diff --git a/.benchmark/course/frontend/services/ๅญฆๅ‘˜่ฟ›ๅบฆ.ts b/.benchmark/course/frontend/services/ๅญฆๅ‘˜่ฟ›ๅบฆ.ts new file mode 100644 index 0000000..5f252af --- /dev/null +++ b/.benchmark/course/frontend/services/ๅญฆๅ‘˜่ฟ›ๅบฆ.ts @@ -0,0 +1,10 @@ +// Auto-generated ๅญฆๅ‘˜่ฟ›ๅบฆ service +import { api } from "./api"; + +export function get() { + return api.get("/api/ๅญฆๅ‘˜่ฟ›ๅบฆ"); +} + +export function post(data: Record) { + return api.post("/api/ๅญฆๅ‘˜่ฟ›ๅบฆ", data); +} diff --git a/.benchmark/course/frontend/services/็ซ ่Š‚็ฎก็†.ts b/.benchmark/course/frontend/services/็ซ ่Š‚็ฎก็†.ts new file mode 100644 index 0000000..2ebdb99 --- /dev/null +++ b/.benchmark/course/frontend/services/็ซ ่Š‚็ฎก็†.ts @@ -0,0 +1,10 @@ +// Auto-generated ็ซ ่Š‚็ฎก็† service +import { api } from "./api"; + +export function get() { + return api.get("/api/็ซ ่Š‚็ฎก็†"); +} + +export function post(data: Record) { + return api.post("/api/็ซ ่Š‚็ฎก็†", data); +} diff --git a/.benchmark/course/frontend/services/่ฏพ็จ‹ๅ‘ๅธƒ.ts b/.benchmark/course/frontend/services/่ฏพ็จ‹ๅ‘ๅธƒ.ts new file mode 100644 index 0000000..e1a2cd2 --- /dev/null +++ b/.benchmark/course/frontend/services/่ฏพ็จ‹ๅ‘ๅธƒ.ts @@ -0,0 +1,10 @@ +// Auto-generated ่ฏพ็จ‹ๅ‘ๅธƒ service +import { api } from "./api"; + +export function get() { + return api.get("/api/่ฏพ็จ‹ๅ‘ๅธƒ"); +} + +export function post(data: Record) { + return api.post("/api/่ฏพ็จ‹ๅ‘ๅธƒ", data); +} diff --git a/.benchmark/course/frontend/tailwind.config.ts b/.benchmark/course/frontend/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/course/frontend/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/course/frontend/tsconfig.json b/.benchmark/course/frontend/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/course/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/course/frontend/types/index.ts b/.benchmark/course/frontend/types/index.ts new file mode 100644 index 0000000..a445063 --- /dev/null +++ b/.benchmark/course/frontend/types/index.ts @@ -0,0 +1,79 @@ +// Auto-generated types for EduPlatform + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/.gitignore b/.benchmark/course/fullstack/.gitignore new file mode 100644 index 0000000..7488f6e --- /dev/null +++ b/.benchmark/course/fullstack/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.next/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/course/fullstack/README.md b/.benchmark/course/fullstack/README.md new file mode 100644 index 0000000..78eae20 --- /dev/null +++ b/.benchmark/course/fullstack/README.md @@ -0,0 +1,97 @@ +# EduPlatform โ€” Fullstack Project + +> ไธ€ไธชๆ”ฏๆŒๅœจ็บฟ่ฏพ็จ‹ๅญฆไน ใ€็›ดๆ’ญๆŽˆ่ฏพใ€้ข˜ๅบ“็ปƒไน ๅ’Œๅญฆๆƒ…่ฟฝ่ธช็š„ๆ•™่‚ฒๅนณๅฐใ€‚ + +## Architecture + +``` +apps/ +โ”œโ”€โ”€ web/ # Next.js 15 + TypeScript + Tailwind CSS +โ””โ”€โ”€ api/ # Fastify 5 + TypeScript + SQLite (sql.js) + +packages/ +โ”œโ”€โ”€ shared-types/ # @shared/types โ€” shared TypeScript interfaces +โ””โ”€โ”€ shared-config/ # @shared/config โ€” shared configuration +``` + +## Quick Start + +```bash +# Install all dependencies (root + workspaces) +npm install + +# Start both frontend and backend in dev mode +npm run dev + +# Or start individually +npm run dev:web # http://localhost:3000 +npm run dev:api # http://localhost:3001 +``` + +## Build + +```bash +# Build everything +npm run build + +# Or individually +npm run build:web +npm run build:api +``` + +## Testing + +```bash +# Run API tests +npm test +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` +- `POST /api/auth/login` +- `GET /api/auth/me` + +### Resources +#### users +- `GET /api/users` โ€” List +- `GET /api/users/:id` โ€” Get +- `POST /api/users` โ€” Create +- `PUT /api/users/:id` โ€” Update +- `DELETE /api/users/:id` โ€” Delete + +#### courses +- `GET /api/courses` โ€” List +- `GET /api/courses/:id` โ€” Get +- `POST /api/courses` โ€” Create +- `PUT /api/courses/:id` โ€” Update +- `DELETE /api/courses/:id` โ€” Delete + +#### chapters +- `GET /api/chapters` โ€” List +- `GET /api/chapters/:id` โ€” Get +- `POST /api/chapters` โ€” Create +- `PUT /api/chapters/:id` โ€” Update +- `DELETE /api/chapters/:id` โ€” Delete + +#### students +- `GET /api/students` โ€” List +- `GET /api/students/:id` โ€” Get +- `POST /api/students` โ€” Create +- `PUT /api/students/:id` โ€” Update +- `DELETE /api/students/:id` โ€” Delete + +#### assignments +- `GET /api/assignments` โ€” List +- `GET /api/assignments/:id` โ€” Get +- `POST /api/assignments` โ€” Create +- `PUT /api/assignments/:id` โ€” Update +- `DELETE /api/assignments/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check + +## Environment Variables + +See `.env.example` for all available configuration. diff --git a/.benchmark/course/fullstack/apps/api/.gitignore b/.benchmark/course/fullstack/apps/api/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/course/fullstack/apps/api/README.md b/.benchmark/course/fullstack/apps/api/README.md new file mode 100644 index 0000000..879ffbb --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/README.md @@ -0,0 +1,90 @@ +# EduPlatform โ€” Backend API + +> ไธ€ไธชๆ”ฏๆŒๅœจ็บฟ่ฏพ็จ‹ๅญฆไน ใ€็›ดๆ’ญๆŽˆ่ฏพใ€้ข˜ๅบ“็ปƒไน ๅ’Œๅญฆๆƒ…่ฟฝ่ธช็š„ๆ•™่‚ฒๅนณๅฐใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### courses +- `GET /api/courses` โ€” List all +- `GET /api/courses/:id` โ€” Get by ID +- `POST /api/courses` โ€” Create +- `PUT /api/courses/:id` โ€” Update +- `DELETE /api/courses/:id` โ€” Delete + +#### chapters +- `GET /api/chapters` โ€” List all +- `GET /api/chapters/:id` โ€” Get by ID +- `POST /api/chapters` โ€” Create +- `PUT /api/chapters/:id` โ€” Update +- `DELETE /api/chapters/:id` โ€” Delete + +#### students +- `GET /api/students` โ€” List all +- `GET /api/students/:id` โ€” Get by ID +- `POST /api/students` โ€” Create +- `PUT /api/students/:id` โ€” Update +- `DELETE /api/students/:id` โ€” Delete + +#### assignments +- `GET /api/assignments` โ€” List all +- `GET /api/assignments/:id` โ€” Get by ID +- `POST /api/assignments` โ€” Create +- `PUT /api/assignments/:id` โ€” Update +- `DELETE /api/assignments/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/course/fullstack/apps/api/package.json b/.benchmark/course/fullstack/apps/api/package.json new file mode 100644 index 0000000..2fa96e8 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "eduplatform-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts new file mode 100644 index 0000000..5ff6b9f --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/courses.test.ts @@ -0,0 +1,122 @@ +// Courses CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "title": "sample-title", + "description": "sample-description", + "instructor": "sample-instructor", + "price": 1, + "coverUrl": "sample-coverurl", + "category": "sample-category" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/courses", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/courses", () => { + it("creates a cours", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/courses", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/courses/:id", () => { + it("returns the created cours", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/courses/:id", () => { + it("updates the cours", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/courses/:id", () => { + it("deletes the cours", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/courses/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts new file mode 100644 index 0000000..3845bdf --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/exercises.test.ts @@ -0,0 +1,120 @@ +// Exercis CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/exercises", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/exercises", () => { + it("creates a exercis", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/exercises", + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/exercises/:id", () => { + it("returns the created exercis", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/exercises/:id", () => { + it("updates the exercis", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "lessonId": "00000000-0000-0000-0000-000000000001", + "question": "sample-question", + "options": "sample-options", + "answer": "sample-answer" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/exercises/:id", () => { + it("deletes the exercis", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/exercises/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts new file mode 100644 index 0000000..adcd9d7 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/lessons.test.ts @@ -0,0 +1,122 @@ +// Lesson CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/lessons", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/lessons", () => { + it("creates a lesson", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/lessons", + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/lessons/:id", () => { + it("returns the created lesson", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/lessons/:id", () => { + it("updates the lesson", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "courseId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "videoUrl": "sample-videourl", + "durationSec": 1, + "sortOrder": 1 + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/lessons/:id", () => { + it("deletes the lesson", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/lessons/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts new file mode 100644 index 0000000..86811c6 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/user_progress.test.ts @@ -0,0 +1,122 @@ +// UserProgress CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/user_progress", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/user_progress", () => { + it("creates a user_progress", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/user_progress", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/user_progress/:id", () => { + it("returns the created user_progress", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/user_progress/:id", () => { + it("updates the user_progress", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "lessonId": "00000000-0000-0000-0000-000000000001", + "completed": true, + "watchedSec": 1, + "UNIQUE(userId, lessonId)": "sample-unique(userid, lessonid)" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/user_progress/:id", () => { + it("deletes the user_progress", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/user_progress/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts b/.benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/course/fullstack/apps/api/src/db/client.ts b/.benchmark/course/fullstack/apps/api/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/course/fullstack/apps/api/src/db/schema.ts b/.benchmark/course/fullstack/apps/api/src/db/schema.ts new file mode 100644 index 0000000..57e05a7 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/db/schema.ts @@ -0,0 +1,16 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS courses (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n instructor TEXT,\n price REAL,\n cover_url TEXT,\n category TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS chapters (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n sort_order INTEGER,\n duration_min INTEGER,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS students (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n enrolled_at TEXT,\n progress REAL,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS assignments (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n course_id TEXT REFERENCES courses(id),\n title TEXT NOT NULL,\n description TEXT,\n due_date TEXT,\n max_score INTEGER,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/index.ts b/.benchmark/course/fullstack/apps/api/src/index.ts new file mode 100644 index 0000000..45ddcfb --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/index.ts @@ -0,0 +1,71 @@ +// EduPlatform โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { coursesRoutes } from "./routes/courses.js"; +import { chaptersRoutes } from "./routes/chapters.js"; +import { studentsRoutes } from "./routes/students.js"; +import { assignmentsRoutes } from "./routes/assignments.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-0b384181"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(coursesRoutes, { prefix: "/api/courses" }); + await app.register(chaptersRoutes, { prefix: "/api/chapters" }); + await app.register(studentsRoutes, { prefix: "/api/students" }); + await app.register(assignmentsRoutes, { prefix: "/api/assignments" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/course/fullstack/apps/api/src/middleware/auth.ts b/.benchmark/course/fullstack/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/auth.ts b/.benchmark/course/fullstack/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/courses.ts b/.benchmark/course/fullstack/apps/api/src/routes/courses.ts new file mode 100644 index 0000000..4ec97c0 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/courses.ts @@ -0,0 +1,51 @@ +// Auto-generated Courses routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CoursesService } from "../services/cours.js"; +import type { CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +const service = new CoursesService(); + +export async function coursesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/courses โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/courses/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/courses โ€” create + app.post<{ Body: CreateCoursesInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/courses/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCoursesInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/courses/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Courses not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/exercises.ts b/.benchmark/course/fullstack/apps/api/src/routes/exercises.ts new file mode 100644 index 0000000..352bcb9 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/exercises.ts @@ -0,0 +1,51 @@ +// Auto-generated Exercis routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { ExercisService } from "../services/exercis.js"; +import type { CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +const service = new ExercisService(); + +export async function exercisesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/exercises โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/exercises/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/exercises โ€” create + app.post<{ Body: CreateExercisInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/exercises/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateExercisInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/exercises/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Exercis not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/lessons.ts b/.benchmark/course/fullstack/apps/api/src/routes/lessons.ts new file mode 100644 index 0000000..22468bd --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/lessons.ts @@ -0,0 +1,51 @@ +// Auto-generated Lesson routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { LessonService } from "../services/lesson.js"; +import type { CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +const service = new LessonService(); + +export async function lessonsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/lessons โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/lessons/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/lessons โ€” create + app.post<{ Body: CreateLessonInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/lessons/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateLessonInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/lessons/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Lesson not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/user_progress.ts b/.benchmark/course/fullstack/apps/api/src/routes/user_progress.ts new file mode 100644 index 0000000..ac9fec6 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/user_progress.ts @@ -0,0 +1,51 @@ +// Auto-generated UserProgress routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserProgressService } from "../services/user_progress.js"; +import type { CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +const service = new UserProgressService(); + +export async function user_progressRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/user_progress โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/user_progress/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/user_progress โ€” create + app.post<{ Body: CreateUserProgressInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/user_progress/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserProgressInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/user_progress/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "UserProgress not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/routes/users.ts b/.benchmark/course/fullstack/apps/api/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/cours.ts b/.benchmark/course/fullstack/apps/api/src/services/cours.ts new file mode 100644 index 0000000..38e9af2 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/cours.ts @@ -0,0 +1,57 @@ +// Auto-generated Courses service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Courses, CreateCoursesInput, UpdateCoursesInput } from "../types/index.js"; + +export class CoursesService { + /** List all courses */ + list(): Courses[] { + return queryAll("SELECT * FROM courses ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Courses | undefined { + return queryOne("SELECT * FROM courses WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCoursesInput): Courses { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "title", "description", "instructor", "price", "cover_url", "category", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.instructor ?? null, input.price ?? null, input.coverUrl ?? null, input.category ?? null, now, now]; + + execute(`INSERT INTO courses (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCoursesInput): Courses | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.instructor !== undefined) { sets.push("instructor = ?"); values.push(input.instructor); } + if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); } + if (input.coverUrl !== undefined) { sets.push("cover_url = ?"); values.push(input.coverUrl); } + if (input.category !== undefined) { sets.push("category = ?"); values.push(input.category); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE courses SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM courses WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/exercis.ts b/.benchmark/course/fullstack/apps/api/src/services/exercis.ts new file mode 100644 index 0000000..c6bef85 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/exercis.ts @@ -0,0 +1,56 @@ +// Auto-generated Exercis service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Exercis, CreateExercisInput, UpdateExercisInput } from "../types/index.js"; + +export class ExercisService { + /** List all exercises */ + list(): Exercis[] { + return queryAll("SELECT * FROM exercises ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Exercis | undefined { + return queryOne("SELECT * FROM exercises WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateExercisInput): Exercis { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "lesson_id", "question", "options", "answer"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.lessonId ?? null, input.question ?? null, input.options ?? null, input.answer ?? null]; + + execute(`INSERT INTO exercises (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateExercisInput): Exercis | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.question !== undefined) { sets.push("question = ?"); values.push(input.question); } + if (input.options !== undefined) { sets.push("options = ?"); values.push(input.options); } + if (input.answer !== undefined) { sets.push("answer = ?"); values.push(input.answer); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE exercises SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM exercises WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/lesson.ts b/.benchmark/course/fullstack/apps/api/src/services/lesson.ts new file mode 100644 index 0000000..2ddf9ab --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/lesson.ts @@ -0,0 +1,57 @@ +// Auto-generated Lesson service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Lesson, CreateLessonInput, UpdateLessonInput } from "../types/index.js"; + +export class LessonService { + /** List all lessons */ + list(): Lesson[] { + return queryAll("SELECT * FROM lessons ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Lesson | undefined { + return queryOne("SELECT * FROM lessons WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateLessonInput): Lesson { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "course_id", "title", "video_url", "duration_sec", "sort_order"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.courseId ?? null, input.title ?? null, input.videoUrl ?? null, input.durationSec ?? null, input.sortOrder ?? null]; + + execute(`INSERT INTO lessons (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateLessonInput): Lesson | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.courseId !== undefined) { sets.push("course_id = ?"); values.push(input.courseId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.videoUrl !== undefined) { sets.push("video_url = ?"); values.push(input.videoUrl); } + if (input.durationSec !== undefined) { sets.push("duration_sec = ?"); values.push(input.durationSec); } + if (input.sortOrder !== undefined) { sets.push("sort_order = ?"); values.push(input.sortOrder); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE lessons SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM lessons WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/user.ts b/.benchmark/course/fullstack/apps/api/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/services/user_progress.ts b/.benchmark/course/fullstack/apps/api/src/services/user_progress.ts new file mode 100644 index 0000000..a14a7c8 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/services/user_progress.ts @@ -0,0 +1,56 @@ +// Auto-generated UserProgress service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { UserProgress, CreateUserProgressInput, UpdateUserProgressInput } from "../types/index.js"; + +export class UserProgressService { + /** List all user_progress */ + list(): UserProgress[] { + return queryAll("SELECT * FROM user_progress ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): UserProgress | undefined { + return queryOne("SELECT * FROM user_progress WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserProgressInput): UserProgress { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "user_id", "lesson_id", "completed", "watched_sec"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.lessonId ?? null, input.completed ?? null, input.watchedSec ?? null]; + + execute(`INSERT INTO user_progress (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserProgressInput): UserProgress | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.lessonId !== undefined) { sets.push("lesson_id = ?"); values.push(input.lessonId); } + if (input.completed !== undefined) { sets.push("completed = ?"); values.push(input.completed); } + if (input.watchedSec !== undefined) { sets.push("watched_sec = ?"); values.push(input.watchedSec); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE user_progress SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM user_progress WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/types/fastify.d.ts b/.benchmark/course/fullstack/apps/api/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/course/fullstack/apps/api/src/types/index.ts b/.benchmark/course/fullstack/apps/api/src/types/index.ts new file mode 100644 index 0000000..16ebe48 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/types/index.ts @@ -0,0 +1,131 @@ +// Import and re-export shared entity types +import type { + User, + Courses, + Chapters, + Students, + Assignments, + ApiResponse, + PaginatedResponse, + ErrorResponse, +} from "@shared/types"; + +export type { + User, + Courses, + Chapters, + Students, + Assignments, + ApiResponse, + PaginatedResponse, + ErrorResponse, +}; + +// Auto-generated types (from Model Contract) + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateCoursesInput { + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface CreateChaptersInput { + userId?: string; + courseId?: string; + title: string; + durationMin?: number; +} + +export interface CreateStudentsInput { + userId?: string; + name: string; + email?: string; +} + +export interface CreateAssignmentsInput { + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateCoursesInput { + userId?: string; + title?: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; +} + +export interface UpdateChaptersInput { + userId?: string; + courseId?: string; + title?: string; + durationMin?: number; +} + +export interface UpdateStudentsInput { + userId?: string; + name?: string; + email?: string; +} + +export interface UpdateAssignmentsInput { + userId?: string; + courseId?: string; + title?: string; + description?: string; + dueDate?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts b/.benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/course/fullstack/apps/api/tsconfig.json b/.benchmark/course/fullstack/apps/api/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/course/fullstack/apps/api/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx b/.benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/course/[id]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 ๅฎŒๅ…จๆŒ‡ๅ—", instructor: "ๅผ ่€ๅธˆ", price: 199, cover: "๐Ÿ“˜" }, + { id: "c2", title: "TypeScript ้ซ˜็บง็ผ–็จ‹", instructor: "ๆŽ่€ๅธˆ", price: 149, cover: "๐Ÿ“™" }, + { id: "c3", title: "Node.js ๅŽ็ซฏๅฎžๆˆ˜", instructor: "็Ž‹่€ๅธˆ", price: 249, cover: "๐Ÿ“—" }, +]; + +export default function CoursePage() { + return ( +
+

่ฏพ็จ‹ไธญๅฟƒ

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

ยฅ{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/exercises/page.tsx b/.benchmark/course/fullstack/apps/web/app/exercises/page.tsx new file mode 100644 index 0000000..15d71e4 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/exercises/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function ExercisePage() { + return ( +
+

้ข˜ๅบ“็ปƒไน 

+

็ซ ่Š‚็ปƒไน ๅ’Œๆจกๆ‹Ÿ่€ƒ่ฏ•ๅŠŸ่ƒฝ

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/globals.css b/.benchmark/course/fullstack/apps/web/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/course/fullstack/apps/web/app/layout.tsx b/.benchmark/course/fullstack/apps/web/app/layout.tsx new file mode 100644 index 0000000..8be9dca --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "EduPlatform", + description: "ไธ€ไธชๆ”ฏๆŒๅœจ็บฟ่ฏพ็จ‹ๅญฆไน ใ€็›ดๆ’ญๆŽˆ่ฏพใ€้ข˜ๅบ“็ปƒไน ๅ’Œๅญฆๆƒ…่ฟฝ่ธช็š„ๆ•™่‚ฒๅนณๅฐใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx b/.benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx new file mode 100644 index 0000000..6d39efe --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/learn/[courseId]/[lessonId]/page.tsx @@ -0,0 +1,26 @@ +"use client"; +import { Card } from "@/components/ui/Card"; + +const courses = [ + { id: "c1", title: "React 19 ๅฎŒๅ…จๆŒ‡ๅ—", instructor: "ๅผ ่€ๅธˆ", price: 199, cover: "๐Ÿ“˜" }, + { id: "c2", title: "TypeScript ้ซ˜็บง็ผ–็จ‹", instructor: "ๆŽ่€ๅธˆ", price: 149, cover: "๐Ÿ“™" }, + { id: "c3", title: "Node.js ๅŽ็ซฏๅฎžๆˆ˜", instructor: "็Ž‹่€ๅธˆ", price: 249, cover: "๐Ÿ“—" }, +]; + +export default function CoursePage() { + return ( +
+

่ฏพ็จ‹ไธญๅฟƒ

+ {courses.map(c => ( + +
{c.cover}
+
+

{c.title}

+

{c.instructor}

+

ยฅ{c.price}

+
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/live/page.tsx b/.benchmark/course/fullstack/apps/web/app/live/page.tsx new file mode 100644 index 0000000..8679f8f --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/live/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +export default function LivePage() { + return ( +
+

็›ดๆ’ญ่ฏพๅ ‚

+

่ฏพ็จ‹็›ดๆ’ญๅŠŸ่ƒฝ

+
+ ); +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/app/page.tsx b/.benchmark/course/fullstack/apps/web/app/page.tsx new file mode 100644 index 0000000..47bd262 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+ {/* Hero Section */} +
+

EduPlatform

+

ๅบ”็”จ้ฆ–้กต

+
+ + {/* Quick Actions */} +
+

ๅฟซๆทๅ…ฅๅฃ

+
+ + ๐Ÿ“‹ + ่ฏพ็จ‹ๅ‘ๅธƒ + + + ๐Ÿ“… + ็ซ ่Š‚็ฎก็† + + + ๐Ÿ“ + ๅญฆๅ‘˜่ฟ›ๅบฆ + + + ๐Ÿ“ธ + ไฝœไธšๆ‰นๆ”น + +
+
+ + {/* Recent Activity / Stats */} +
+

ไปŠๆ—ฅๆฆ‚่งˆ

+
+
+
+

ๆฌข่ฟŽไฝฟ็”จ EduPlatform

+

ๅผ€ๅง‹ๆŽข็ดขๅŠŸ่ƒฝๅง

+
+ ๐Ÿ‘‹ +
+
+
+
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/app/profile/page.tsx b/.benchmark/course/fullstack/apps/web/app/profile/page.tsx new file mode 100644 index 0000000..ed7c604 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/app/profile/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; + + +export default function PagePage() { + + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆˆ‘็š„

+

ๅญฆไน ๆŠฅๅ‘Šใ€ๅทฒ่ดญ่ฏพ็จ‹ใ€่ฎพ็ฝฎ

+
+ +
+ + { +

ๆˆ‘็š„ ้กต้ขๅ†…ๅฎน

+

่ทฏ็”ฑ: /profile

+
} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx b/.benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx new file mode 100644 index 0000000..1bfce5d --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/layout/BottomNav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +export function BottomNav({ items }: { items: NavItem[] }) { + const pathname = usePathname(); + + if (!items.length) return null; + + return ( + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx b/.benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2580e86 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/layout/Sidebar.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface NavItem { + name: string; + href: string; + icon: string; +} + +interface SidebarProps { + items: NavItem[]; + projectName: string; +} + +export function Sidebar({ items, projectName }: SidebarProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Button.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Button.tsx new file mode 100644 index 0000000..b488663 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Button.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ButtonHTMLAttributes } from "react"; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: "primary" | "secondary" | "danger" | "ghost"; + size?: "sm" | "md" | "lg"; + loading?: boolean; +} + +export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) { + const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400", + danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", + ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400", + }; + const sizes = { + sm: "px-3 py-1.5 text-sm", + md: "px-4 py-2 text-sm", + lg: "px-6 py-3 text-base", + }; + + return ( + + ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Card.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Card.tsx new file mode 100644 index 0000000..131e6d0 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Card.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = "", onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx b/.benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx new file mode 100644 index 0000000..821bf75 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/EmptyState.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from "react"; + +interface EmptyStateProps { + icon?: string; + title: string; + description?: string; + action?: ReactNode; +} + +export function EmptyState({ icon = "๐Ÿ“ญ", title, description, action }: EmptyStateProps) { + return ( +
+ {icon} +

{title}

+ {description &&

{description}

} + {action} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Input.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Input.tsx new file mode 100644 index 0000000..7c782bd --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Input.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { type InputHTMLAttributes } from "react"; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export function Input({ label, error, className = "", id, ...props }: InputProps) { + return ( +
+ {label && } + + {error &&

{error}

} +
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/components/ui/Modal.tsx b/.benchmark/course/fullstack/apps/web/components/ui/Modal.tsx new file mode 100644 index 0000000..3a6bb03 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/components/ui/Modal.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, type ReactNode } from "react"; + +interface ModalProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; +} + +export function Modal({ open, onClose, title, children }: ModalProps) { + useEffect(() => { + if (open) document.body.style.overflow = "hidden"; + else document.body.style.overflow = ""; + return () => { document.body.style.overflow = ""; }; + }, [open]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+ {children} +
+
+ ); +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useCourses.ts b/.benchmark/course/fullstack/apps/web/hooks/useCourses.ts new file mode 100644 index 0000000..1f4fc4a --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useCourses.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for courses +type Cour = Record; + +/** + * Fetch and manage courses data. + */ +export function useCourses() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/courses`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useDebounce.ts b/.benchmark/course/fullstack/apps/web/hooks/useDebounce.ts new file mode 100644 index 0000000..280b55f --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useDebounce.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export function useDebounce(value: T, delay: number = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useExercises.ts b/.benchmark/course/fullstack/apps/web/hooks/useExercises.ts new file mode 100644 index 0000000..d2c16ca --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useExercises.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for exercises +type Exerci = Record; + +/** + * Fetch and manage exercises data. + */ +export function useExercises() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/exercises`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useForm.ts b/.benchmark/course/fullstack/apps/web/hooks/useForm.ts new file mode 100644 index 0000000..639b11c --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useForm.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useState, useCallback } from "react"; + +interface UseFormOptions { + initialValues: T; + onSubmit: (values: T) => Promise; +} + +export function useForm>({ initialValues, onSubmit }: UseFormOptions) { + const [values, setValues] = useState(initialValues); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const setField = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })); + }, []); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + try { + setSubmitting(true); + setError(null); + await onSubmit(values); + } catch (err) { + setError(err instanceof Error ? err.message : "Submit failed"); + } finally { + setSubmitting(false); + } + }, [values, onSubmit]); + + return { values, setField, submitting, error, handleSubmit }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useLessons.ts b/.benchmark/course/fullstack/apps/web/hooks/useLessons.ts new file mode 100644 index 0000000..0bf3571 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useLessons.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for lessons +type Lesson = Record; + +/** + * Fetch and manage lessons data. + */ +export function useLessons() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/lessons`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/hooks/useProgress.ts b/.benchmark/course/fullstack/apps/web/hooks/useProgress.ts new file mode 100644 index 0000000..cdcac25 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/hooks/useProgress.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +// Generic data type for progress +type Progres = Record; + +/** + * Fetch and manage progress data. + */ +export function useProgress() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(`/api/progress`); + const json = await res.json(); + setData(Array.isArray(json) ? json : json?.data || []); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load data"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +} diff --git a/.benchmark/course/fullstack/apps/web/next.config.ts b/.benchmark/course/fullstack/apps/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/.benchmark/course/fullstack/apps/web/package.json b/.benchmark/course/fullstack/apps/web/package.json new file mode 100644 index 0000000..8e6b869 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "eduplatform-web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@shared/types": "*", + "@shared/config": "*" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "typescript": "^5.6.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/postcss.config.js b/.benchmark/course/fullstack/apps/web/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/.benchmark/course/fullstack/apps/web/services/api.ts b/.benchmark/course/fullstack/apps/web/services/api.ts new file mode 100644 index 0000000..bb30f14 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/api.ts @@ -0,0 +1,49 @@ +// Auto-generated API client for EduPlatform + +import { API_BASE_URL, API_PREFIX } from "@shared/config"; + +const API_BASE = `${API_BASE_URL}${API_PREFIX}`; + +interface RequestOptions extends RequestInit { + params?: Record; +} + +async function request(endpoint: string, options: RequestOptions = {}): Promise { + const { params, ...init } = options; + let url = `${API_BASE}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v))); + url += `?${searchParams.toString()}`; + } + + const res = await fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message || `API Error: ${res.status}`); + } + + return res.json(); +} + +export const api = { + get: (url: string, params?: Record) => + request(url, { method: "GET", params }), + + post: (url: string, data?: unknown) => + request(url, { method: "POST", body: JSON.stringify(data) }), + + put: (url: string, data?: unknown) => + request(url, { method: "PUT", body: JSON.stringify(data) }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/.benchmark/course/fullstack/apps/web/services/courses.ts b/.benchmark/course/fullstack/apps/web/services/courses.ts new file mode 100644 index 0000000..6406396 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/courses.ts @@ -0,0 +1,10 @@ +// Auto-generated courses service +import { api } from "./api"; + +export function get() { + return api.get("/api/courses"); +} + +export function post(data: Record) { + return api.post("/api/courses", data); +} diff --git a/.benchmark/course/fullstack/apps/web/services/exercises.ts b/.benchmark/course/fullstack/apps/web/services/exercises.ts new file mode 100644 index 0000000..4bd54bd --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/exercises.ts @@ -0,0 +1,10 @@ +// Auto-generated exercises service +import { api } from "./api"; + +export function get() { + return api.get("/api/exercises"); +} + +export function postsubmit(data: Record) { + return api.post("/api/exercises", data); +} diff --git a/.benchmark/course/fullstack/apps/web/services/lessons.ts b/.benchmark/course/fullstack/apps/web/services/lessons.ts new file mode 100644 index 0000000..a6e1315 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/lessons.ts @@ -0,0 +1,6 @@ +// Auto-generated lessons service +import { api } from "./api"; + +export function get_id(id: string) { + return api.get(`/api/lessons/${id}`); +} diff --git a/.benchmark/course/fullstack/apps/web/services/progress.ts b/.benchmark/course/fullstack/apps/web/services/progress.ts new file mode 100644 index 0000000..a88a74b --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/services/progress.ts @@ -0,0 +1,6 @@ +// Auto-generated progress service +import { api } from "./api"; + +export function get() { + return api.get("/api/progress"); +} diff --git a/.benchmark/course/fullstack/apps/web/tailwind.config.ts b/.benchmark/course/fullstack/apps/web/tailwind.config.ts new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + extend: {}, + }, + plugins: [], +}; +export default config; diff --git a/.benchmark/course/fullstack/apps/web/tsconfig.json b/.benchmark/course/fullstack/apps/web/tsconfig.json new file mode 100644 index 0000000..9c7e071 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/apps/web/types/index.ts b/.benchmark/course/fullstack/apps/web/types/index.ts new file mode 100644 index 0000000..a445063 --- /dev/null +++ b/.benchmark/course/fullstack/apps/web/types/index.ts @@ -0,0 +1,79 @@ +// Auto-generated types for EduPlatform + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + +// โ”€โ”€โ”€ Base โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id: string; + phone: string; + nickname: string; + avatarUrl: string; + createdAt: string; + updatedAt: string; +} +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + + +// โ”€โ”€โ”€ API Responses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message: string; +} + +export interface PaginatedResponse extends ApiResponse { + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/package.json b/.benchmark/course/fullstack/package.json new file mode 100644 index 0000000..ff9f0bf --- /dev/null +++ b/.benchmark/course/fullstack/package.json @@ -0,0 +1,19 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "node scripts/dev.mjs", + "build": "node scripts/build.mjs", + "dev:web": "npm run dev -w apps/web", + "dev:api": "npm run dev -w apps/api", + "build:web": "npm run build -w apps/web", + "build:api": "npm run build -w apps/api", + "test": "npm run test -w apps/api", + "lint": "npm run lint -w apps/web" + } +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-config/package.json b/.benchmark/course/fullstack/packages/shared-config/package.json new file mode 100644 index 0000000..c2f52a9 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/config", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-config/src/index.ts b/.benchmark/course/fullstack/packages/shared-config/src/index.ts new file mode 100644 index 0000000..abf84f0 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-config/src/index.ts @@ -0,0 +1,16 @@ +// Shared configuration for fullstack project + +/** API base URL โ€” reads from env or defaults */ +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** API prefix for all endpoints */ +export const API_PREFIX = "/api"; + +/** Full API URL */ +export const API_URL = `${API_BASE_URL}${API_PREFIX}`; + +/** Auth token storage key */ +export const AUTH_TOKEN_KEY = "auth_token"; + +/** Default page size for paginated endpoints */ +export const DEFAULT_PAGE_SIZE = 20; diff --git a/.benchmark/course/fullstack/packages/shared-config/tsconfig.json b/.benchmark/course/fullstack/packages/shared-config/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-types/package.json b/.benchmark/course/fullstack/packages/shared-types/package.json new file mode 100644 index 0000000..d7fb84c --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@shared/types", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/packages/shared-types/src/index.ts b/.benchmark/course/fullstack/packages/shared-types/src/index.ts new file mode 100644 index 0000000..5ef3530 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-types/src/index.ts @@ -0,0 +1,81 @@ +// Shared types for fullstack project +// Auto-generated โ€” used by both apps/web and apps/api + +// โ”€โ”€โ”€ Entities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Courses { + id: string; + userId?: string; + title: string; + description?: string; + instructor?: string; + price?: number; + coverUrl?: string; + category?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Chapters { + id: string; + userId?: string; + courseId?: string; + title: string; + sortOrder?: number; + durationMin?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Students { + id: string; + userId?: string; + name: string; + email?: string; + enrolledAt?: string; + progress?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface Assignments { + id: string; + userId?: string; + courseId?: string; + title: string; + description?: string; + dueDate?: string; + maxScore?: number; + createdAt?: string; + updatedAt?: string; +} + +// โ”€โ”€โ”€ API Response Wrappers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} diff --git a/.benchmark/course/fullstack/packages/shared-types/tsconfig.json b/.benchmark/course/fullstack/packages/shared-types/tsconfig.json new file mode 100644 index 0000000..663a559 --- /dev/null +++ b/.benchmark/course/fullstack/packages/shared-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/.benchmark/course/fullstack/scripts/build.mjs b/.benchmark/course/fullstack/scripts/build.mjs new file mode 100644 index 0000000..84fa093 --- /dev/null +++ b/.benchmark/course/fullstack/scripts/build.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Build script โ€” builds both web and api. + * Usage: node scripts/build.mjs + */ + +import { execSync } from "node:child_process"; + +const ROOT = new URL("..", import.meta.url).pathname; + +function run(cmd, cwd) { + console.log(`\n๐Ÿ”จ ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("๐Ÿ—๏ธ Building fullstack project...\n"); + +try { + run("npm run build", `${ROOT}/apps/api`); + run("npm run build", `${ROOT}/apps/web`); + console.log("\nโœ… Build complete!"); +} catch (e) { + console.error("\nโŒ Build failed:", e.message); + process.exit(1); +} diff --git a/.benchmark/course/fullstack/scripts/dev.mjs b/.benchmark/course/fullstack/scripts/dev.mjs new file mode 100644 index 0000000..dcb8118 --- /dev/null +++ b/.benchmark/course/fullstack/scripts/dev.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +/** + * Dev script โ€” starts both web and api in parallel. + * Usage: node scripts/dev.mjs + */ + +import { spawn } from "node:child_process"; + +function start(name, command, args, cwd) { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: true, + env: { ...process.env, FORCE_COLOR: "1" }, + }); + child.on("error", (err) => console.error(`[${name}] Failed: ${err.message}`)); + child.on("exit", (code) => { + if (code !== 0 && code !== null) console.error(`[${name}] Exited with code ${code}`); + }); + return child; +} + +const ROOT = new URL("..", import.meta.url).pathname; + +console.log("๐Ÿš€ Starting fullstack dev servers...\n"); + +const api = start("api", "npm", ["run", "dev"], `${ROOT}/apps/api`); +const web = start("web", "npm", ["run", "dev"], `${ROOT}/apps/web`); + +process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); }); +process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); }); diff --git a/.benchmark/course/prd.json b/.benchmark/course/prd.json new file mode 100644 index 0000000..affd9d4 --- /dev/null +++ b/.benchmark/course/prd.json @@ -0,0 +1 @@ +{"projectName":"EduPlatform","chineseName":"ๅœจ็บฟๆ•™่‚ฒๅนณๅฐ","domain":"education","matchConfidence":"high","summary":"ไธ€ไธชๆ”ฏๆŒๅœจ็บฟ่ฏพ็จ‹ๅญฆไน ใ€็›ดๆ’ญๆŽˆ่ฏพใ€้ข˜ๅบ“็ปƒไน ๅ’Œๅญฆๆƒ…่ฟฝ่ธช็š„ๆ•™่‚ฒๅนณๅฐใ€‚","personas":[{"name":"ๅญฆ็”Ÿ","description":"ๅธŒๆœ›้€š่ฟ‡็บฟไธŠๅญฆไน ๆๅ‡ๆŠ€่ƒฝ็š„ๅœจๆ ก็”Ÿๆˆ–่Œๅœบไบบ","painPoints":["่ฏพ็จ‹่ดจ้‡ๅ‚ๅทฎไธ้ฝ","็ผบไนๅญฆไน ๅŠจๅŠ›","็–‘ๆƒ‘ๆ— ไบบ่งฃ็ญ”"]},{"name":"่ฎฒๅธˆ","description":"ๅ‘ๅธƒ่ฏพ็จ‹ๅ†…ๅฎน็š„ไธ“ไธš่ฎฒๅธˆ","painPoints":["่ฏพไปถ็ฎก็†ไธไพฟ","ๅญฆ็”Ÿไบ’ๅŠจๆ•ˆ็އไฝŽ"]}],"userStories":[{"id":"US-001","as":"ๅญฆ็”Ÿ","want":"่ฏพ็จ‹ๅ‘ๅธƒ","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่ฏพ็จ‹่ดจ้‡ๅ‚ๅทฎไธ้ฝ","priority":"P0"},{"id":"US-002","as":"ๅญฆ็”Ÿ","want":"็ซ ่Š‚็ฎก็†","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่ฏพ็จ‹่ดจ้‡ๅ‚ๅทฎไธ้ฝ","priority":"P0"},{"id":"US-003","as":"ๅญฆ็”Ÿ","want":"ๅญฆๅ‘˜่ฟ›ๅบฆ","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่ฏพ็จ‹่ดจ้‡ๅ‚ๅทฎไธ้ฝ","priority":"P0"},{"id":"US-004","as":"่ฎฒๅธˆ","want":"่ฏพ็จ‹ๅ‘ๅธƒ","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่ฏพไปถ็ฎก็†ไธไพฟ","priority":"P0"},{"id":"US-005","as":"่ฎฒๅธˆ","want":"็ซ ่Š‚็ฎก็†","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่ฏพไปถ็ฎก็†ไธไพฟ","priority":"P0"},{"id":"US-006","as":"่ฎฒๅธˆ","want":"ๅญฆๅ‘˜่ฟ›ๅบฆ","soThat":"่งฃๅ†ณ็—›็‚น๏ผš่ฏพไปถ็ฎก็†ไธไพฟ","priority":"P0"}],"mvpScope":{"description":"MVP ่š็„ฆ ่ฏพ็จ‹ๅ‘ๅธƒใ€็ซ ่Š‚็ฎก็†ใ€ๅญฆๅ‘˜่ฟ›ๅบฆใ€ไฝœไธšๆ‰นๆ”น๏ผŒๅŒ…ๅซ้กต้ข๏ผš้ฆ–้กตใ€่ฏพ็จ‹ๅ‘ๅธƒใ€็ซ ่Š‚็ฎก็†","features":["่ฏพ็จ‹ๅ‘ๅธƒ","็ซ ่Š‚็ฎก็†","ๅญฆๅ‘˜่ฟ›ๅบฆ","ไฝœไธšๆ‰นๆ”น"],"pages":["้ฆ–้กต","่ฏพ็จ‹ๅ‘ๅธƒ","็ซ ่Š‚็ฎก็†"],"estimatedWeeks":3},"features":[{"name":"่ฏพ็จ‹ๅ‘ๅธƒ","description":"่ฏพ็จ‹ๅ‘ๅธƒ","priority":"P0"},{"name":"็ซ ่Š‚็ฎก็†","description":"็ซ ่Š‚็ฎก็†","priority":"P0"},{"name":"ๅญฆๅ‘˜่ฟ›ๅบฆ","description":"ๅญฆๅ‘˜่ฟ›ๅบฆ","priority":"P0"},{"name":"ไฝœไธšๆ‰นๆ”น","description":"ไฝœไธšๆ‰นๆ”น","priority":"P0"}],"pages":[{"name":"้ฆ–้กต","route":"/home","description":"ๅบ”็”จ้ฆ–้กต"},{"name":"่ฏพ็จ‹ๅ‘ๅธƒ","route":"/courses","description":"่ฏพ็จ‹ๅ‘ๅธƒ"},{"name":"็ซ ่Š‚็ฎก็†","route":"/chapters","description":"็ซ ่Š‚็ฎก็†"},{"name":"ๅญฆๅ‘˜่ฟ›ๅบฆ","route":"/students","description":"ๅญฆๅ‘˜่ฟ›ๅบฆ"},{"name":"ไฝœไธšๆ‰นๆ”น","route":"/assignments","description":"ไฝœไธšๆ‰นๆ”น"}],"apiRequirements":[{"method":"GET","path":"/api/courses","description":"่Žทๅ–่ฏพ็จ‹ๅ‘ๅธƒๅˆ—่กจ"},{"method":"POST","path":"/api/courses","description":"ๅˆ›ๅปบ่ฏพ็จ‹ๅ‘ๅธƒ"},{"method":"GET","path":"/api/chapters","description":"่Žทๅ–็ซ ่Š‚็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/chapters","description":"ๅˆ›ๅปบ็ซ ่Š‚็ฎก็†"},{"method":"GET","path":"/api/students","description":"่Žทๅ–ๅญฆๅ‘˜่ฟ›ๅบฆๅˆ—่กจ"},{"method":"POST","path":"/api/students","description":"ๅˆ›ๅปบๅญฆๅ‘˜่ฟ›ๅบฆ"},{"method":"GET","path":"/api/assignments","description":"่Žทๅ–ไฝœไธšๆ‰นๆ”นๅˆ—่กจ"},{"method":"POST","path":"/api/assignments","description":"ๅˆ›ๅปบไฝœไธšๆ‰นๆ”น"},{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}],"techConstraints":{"platforms":["mobile","web"],"recommendedStack":"React Native / Flutter๏ผˆ่ทจๅนณๅฐ๏ผ‰","considerations":["ๅปบ่ฎฎ่ทจๅนณๅฐๆก†ๆžถๅ‡ๅฐ‘ๅผ€ๅ‘ๆˆๆœฌ"]},"extraFeatures":[],"devTasks":[{"id":"T-001","title":"้กน็›ฎ่„šๆ‰‹ๆžถๆญๅปบ","description":"ๅˆๅง‹ๅŒ– EduPlatform ้กน็›ฎ็ป“ๆž„๏ผŒ้…็ฝฎๆž„ๅปบๅทฅๅ…ทๅ’Œ CI/CD","phase":"Foundation","estimatedHours":8,"priority":"P0"},{"id":"T-002","title":"ๆ•ฐๆฎๅบ“่ฎพ่ฎกไธŽๅˆๅง‹ๅŒ–","description":"่ฎพ่ฎกๆ•ฐๆฎๆจกๅž‹๏ผŒๅˆ›ๅปบๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌ","phase":"Foundation","estimatedHours":16,"priority":"P0"},{"id":"T-003","title":"้กต้ขๅผ€ๅ‘๏ผš้ฆ–้กต","description":"ๅผ€ๅ‘ ้ฆ–้กต ้กต้ข๏ผˆ/home๏ผ‰๏ผšๅบ”็”จ้ฆ–้กต","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-004","title":"้กต้ขๅผ€ๅ‘๏ผš่ฏพ็จ‹ๅ‘ๅธƒ","description":"ๅผ€ๅ‘ ่ฏพ็จ‹ๅ‘ๅธƒ ้กต้ข๏ผˆ/courses๏ผ‰๏ผš่ฏพ็จ‹ๅ‘ๅธƒ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-005","title":"้กต้ขๅผ€ๅ‘๏ผš็ซ ่Š‚็ฎก็†","description":"ๅผ€ๅ‘ ็ซ ่Š‚็ฎก็† ้กต้ข๏ผˆ/chapters๏ผ‰๏ผš็ซ ่Š‚็ฎก็†","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-006","title":"้กต้ขๅผ€ๅ‘๏ผšๅญฆๅ‘˜่ฟ›ๅบฆ","description":"ๅผ€ๅ‘ ๅญฆๅ‘˜่ฟ›ๅบฆ ้กต้ข๏ผˆ/students๏ผ‰๏ผšๅญฆๅ‘˜่ฟ›ๅบฆ","phase":"UI Development","estimatedHours":8,"priority":"P0"},{"id":"T-007","title":"API ๅผ€ๅ‘๏ผšGET /api/courses","description":"่Žทๅ–่ฏพ็จ‹ๅ‘ๅธƒๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-008","title":"API ๅผ€ๅ‘๏ผšPOST /api/courses","description":"ๅˆ›ๅปบ่ฏพ็จ‹ๅ‘ๅธƒ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-009","title":"API ๅผ€ๅ‘๏ผšGET /api/chapters","description":"่Žทๅ–็ซ ่Š‚็ฎก็†ๅˆ—่กจ","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-010","title":"API ๅผ€ๅ‘๏ผšPOST /api/chapters","description":"ๅˆ›ๅปบ็ซ ่Š‚็ฎก็†","phase":"Backend Development","estimatedHours":4,"priority":"P0"},{"id":"T-011","title":"ๅ‰ๅŽ็ซฏ่”่ฐƒ","description":"ๅ‰็ซฏ้กต้ขไธŽๅŽ็ซฏ API ้›†ๆˆ่”่ฐƒ","phase":"Integration","estimatedHours":16,"priority":"P0"},{"id":"T-012","title":"ๆต‹่ฏ•ไธŽไฟฎๅค","description":"ๅŠŸ่ƒฝๆต‹่ฏ•ใ€Bug ไฟฎๅคใ€ๆ€ง่ƒฝไผ˜ๅŒ–","phase":"Testing","estimatedHours":24,"priority":"P1"}],"meta":{"generatedAt":"2026-06-05T22:08:37.397Z","inputLength":33,"domainMatchCount":2,"extractedFeatureCount":4,"extractedPageCount":5,"extractedAPICount":10}} \ No newline at end of file diff --git a/.benchmark/course/release/release/README.md b/.benchmark/course/release/release/README.md new file mode 100644 index 0000000..6e568e8 --- /dev/null +++ b/.benchmark/course/release/release/README.md @@ -0,0 +1,33 @@ +# eduplatform โ€” Release Package + +> Version: 0.1.0 | Build: #1 +> Generated: 2026-06-05T22:08:37.740Z + +## Directory Structure + +``` +release/ +โ”œโ”€โ”€ version.json โ€” Version metadata +โ”œโ”€โ”€ build-info.json โ€” Build environment info +โ”œโ”€โ”€ README.md โ€” This file +โ”œโ”€โ”€ manifests/ +โ”‚ โ””โ”€โ”€ manifest.json โ€” Release manifest with file hashes +โ”œโ”€โ”€ checksums/ +โ”‚ โ””โ”€โ”€ checksums.txt โ€” SHA256 checksums for verification +โ”œโ”€โ”€ release-notes/ +โ”‚ โ””โ”€โ”€ release-notes.md โ€” Human-readable release notes +โ”œโ”€โ”€ windows/ โ€” Windows installers +โ”œโ”€โ”€ macos/ โ€” macOS disk images +โ””โ”€โ”€ linux/ โ€” Linux packages +``` + +## Verification + +```bash +# Verify checksums +cd release/checksums +shasum -a 256 -c checksums.txt +``` + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/course/release/release/build-info.json b/.benchmark/course/release/release/build-info.json new file mode 100644 index 0000000..11eb08b --- /dev/null +++ b/.benchmark/course/release/release/build-info.json @@ -0,0 +1,16 @@ +{ + "nodeVersion": "v26.0.0", + "generatorVersion": "1.0.0", + "buildTime": "2026-06-05T22:08:37.740Z", + "domain": "unknown", + "entityCount": 9, + "routeCount": 1, + "screenCount": 13, + "platforms": [ + "windows", + "macos", + "linux" + ], + "project": "eduplatform", + "version": "0.1.0" +} \ No newline at end of file diff --git a/.benchmark/course/release/release/checksums/checksums.txt b/.benchmark/course/release/release/checksums/checksums.txt new file mode 100644 index 0000000..99ff694 --- /dev/null +++ b/.benchmark/course/release/release/checksums/checksums.txt @@ -0,0 +1,6 @@ +79f6d32cc3a0acd8ad736e978e3a0b209ffb7008f08b87b19e29d31accc0c8e2 windows/eduplatform-setup-0.1.0.exe +0d545bb9fe7696f633360c5c7aef906d39d85fae332c687ba03e0146ab9815e6 windows/eduplatform-0.1.0-portable.exe +a3eefa70bb8f4d7d3dd0d837742859d6a928da07054b38f3be46fccaaceb1cc3 macos/eduplatform-0.1.0.dmg +d7028300e13c25b3253059ba4ce56568083f77fc0d671e0266068d4e971acb6e macos/eduplatform-0.1.0-arm64.dmg +b20e30fb4add3e328351bceff0fd3d82c0c80a503fa478b2d9da4758f0060491 linux/eduplatform-0.1.0.AppImage +61616501f0dc42b4f6ab776a45b3c08e9116eb606389653045adad8e77b0811d linux/eduplatform_0.1.0_amd64.deb diff --git a/.benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage b/.benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage new file mode 100644 index 0000000..25e2ef2 --- /dev/null +++ b/.benchmark/course/release/release/linux/eduplatform-0.1.0.AppImage @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Linux AppImage +This is a placeholder file for the Linux AppImage. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb b/.benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb new file mode 100644 index 0000000..089fc5a --- /dev/null +++ b/.benchmark/course/release/release/linux/eduplatform_0.1.0_amd64.deb @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Linux Debian Package +This is a placeholder file for the Linux .deb package. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg b/.benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg new file mode 100644 index 0000000..902319a --- /dev/null +++ b/.benchmark/course/release/release/macos/eduplatform-0.1.0-arm64.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 macOS ARM64 Disk Image +This is a placeholder file for the macOS ARM64 DMG. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/macos/eduplatform-0.1.0.dmg b/.benchmark/course/release/release/macos/eduplatform-0.1.0.dmg new file mode 100644 index 0000000..0f0dcb8 --- /dev/null +++ b/.benchmark/course/release/release/macos/eduplatform-0.1.0.dmg @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 macOS Disk Image +This is a placeholder file for the macOS DMG installer. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/manifests/manifest.json b/.benchmark/course/release/release/manifests/manifest.json new file mode 100644 index 0000000..c7b94d9 --- /dev/null +++ b/.benchmark/course/release/release/manifests/manifest.json @@ -0,0 +1,47 @@ +{ + "project": "eduplatform", + "version": "0.1.0", + "buildNumber": 1, + "generatedAt": "2026-06-05T22:08:37.739Z", + "generator": "release-builder-agent", + "generatorVersion": "1.0.0", + "platforms": [ + "windows", + "macos", + "linux" + ], + "files": [ + { + "path": "windows/eduplatform-setup-0.1.0.exe", + "size": 146, + "sha256": "79f6d32cc3a0acd8ad736e978e3a0b209ffb7008f08b87b19e29d31accc0c8e2" + }, + { + "path": "windows/eduplatform-0.1.0-portable.exe", + "size": 150, + "sha256": "0d545bb9fe7696f633360c5c7aef906d39d85fae332c687ba03e0146ab9815e6" + }, + { + "path": "macos/eduplatform-0.1.0.dmg", + "size": 142, + "sha256": "a3eefa70bb8f4d7d3dd0d837742859d6a928da07054b38f3be46fccaaceb1cc3" + }, + { + "path": "macos/eduplatform-0.1.0-arm64.dmg", + "size": 144, + "sha256": "d7028300e13c25b3253059ba4ce56568083f77fc0d671e0266068d4e971acb6e" + }, + { + "path": "linux/eduplatform-0.1.0.AppImage", + "size": 135, + "sha256": "b20e30fb4add3e328351bceff0fd3d82c0c80a503fa478b2d9da4758f0060491" + }, + { + "path": "linux/eduplatform_0.1.0_amd64.deb", + "size": 145, + "sha256": "61616501f0dc42b4f6ab776a45b3c08e9116eb606389653045adad8e77b0811d" + } + ], + "totalFiles": 6, + "totalSize": 862 +} \ No newline at end of file diff --git a/.benchmark/course/release/release/release-notes/release-notes.md b/.benchmark/course/release/release/release-notes/release-notes.md new file mode 100644 index 0000000..73796f9 --- /dev/null +++ b/.benchmark/course/release/release/release-notes/release-notes.md @@ -0,0 +1,58 @@ +# eduplatform v0.1.0 + +> Release Builder Agent โ€” SF-07 +> Generated: 2026-06-05 22:08:37 UTC + +## Version + +- **Name:** eduplatform +- **Version:** 0.1.0 +- **Build:** #1 + +## Features + +- assignments +- auth +- chapters +- courses +- exercises +- lessons +- students +- user_progress +- users + +## Build Info + +- **Build Time:** 2026-06-05T22:08:37.740Z +- **Generator:** release-builder-agent v1.0.0 +- **Node Version:** v26.0.0 + +## Platforms + +- โœ… windows +- โœ… macos +- โœ… linux + +## Installation + +### Windows +``` +Download the .exe installer from release/windows/ +``` + +### macOS +``` +Download the .dmg from release/macos/ +``` + +### Linux +``` +Download the .AppImage from release/linux/ +``` + +## Checksums + +See `checksums/checksums.txt` for SHA256 verification. + +--- +*Generated by Release Builder Agent โ€” SF-07* \ No newline at end of file diff --git a/.benchmark/course/release/release/version.json b/.benchmark/course/release/release/version.json new file mode 100644 index 0000000..0bc24d6 --- /dev/null +++ b/.benchmark/course/release/release/version.json @@ -0,0 +1,10 @@ +{ + "name": "eduplatform", + "version": "0.1.0", + "buildNumber": 1, + "platforms": [ + "windows", + "macos", + "linux" + ] +} \ No newline at end of file diff --git a/.benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe b/.benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe new file mode 100644 index 0000000..a0032d5 --- /dev/null +++ b/.benchmark/course/release/release/windows/eduplatform-0.1.0-portable.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Windows Portable +This is a placeholder file for the Windows portable executable. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe b/.benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe new file mode 100644 index 0000000..d12a641 --- /dev/null +++ b/.benchmark/course/release/release/windows/eduplatform-setup-0.1.0.exe @@ -0,0 +1,3 @@ +[PLACEHOLDER] eduplatform v0.1.0 Windows Installer +This is a placeholder file for the Windows NSIS installer. +Generated: 2026-06-05T22:08:37.739Z diff --git a/.benchmark/crm/arch.json b/.benchmark/crm/arch.json new file mode 100644 index 0000000..974e900 --- /dev/null +++ b/.benchmark/crm/arch.json @@ -0,0 +1 @@ +{"projectName":"NoteApp","domain":"note","contract":{"projectName":"NoteApp","domain":"note","entities":[{"name":"User","table":"users","description":"็ณป็ปŸ็”จๆˆท","fields":[{"name":"id","type":"UUID","isPrimary":true,"isAuto":true},{"name":"username","type":"VARCHAR(128)","required":true,"unique":true,"validation":{"minLength":2,"maxLength":50}},{"name":"password_hash","type":"VARCHAR(256)","required":true,"isSecret":true},{"name":"nickname","type":"VARCHAR(128)"},{"name":"role","type":"VARCHAR(32)","defaultValue":"user","enum":["user","admin"]},{"name":"phone","type":"VARCHAR(20)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMP","isAuto":true},{"name":"updated_at","type":"TIMESTAMP","isAuto":true}],"relationships":[]},{"name":"Customers","table":"customers","description":"ๅฎขๆˆท็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"email","type":"VARCHAR(256)","required":false,"isPrimary":false,"isAuto":false},{"name":"phone","type":"VARCHAR(20)","required":false,"isPrimary":false,"isAuto":false},{"name":"company","type":"VARCHAR(128)","required":false,"isPrimary":false,"isAuto":false},{"name":"source","type":"VARCHAR(64)","required":false,"isPrimary":false,"isAuto":false},{"name":"tags","type":"JSONB","required":false,"isPrimary":false,"isAuto":true},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]},{"name":"SalesPipeline","table":"sales_pipeline","description":"้”€ๅ”ฎๆผๆ–—่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"customer_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"customers","fkColumn":"id"},{"name":"stage","type":"VARCHAR(32)","required":true,"isPrimary":false,"isAuto":false},{"name":"amount","type":"DECIMAL(12,2)","required":false,"isPrimary":false,"isAuto":false},{"name":"probability","type":"INTEGER","required":false,"isPrimary":false,"isAuto":false},{"name":"expected_close","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"customers","via":"customer_id"}]},{"name":"FollowUps","table":"follow_ups","description":"่ทŸ่ฟ›่ฎฐๅฝ•่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"customer_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"customers","fkColumn":"id"},{"name":"type","type":"VARCHAR(32)","required":false,"isPrimary":false,"isAuto":false},{"name":"content","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"next_action","type":"VARCHAR(256)","required":false,"isPrimary":false,"isAuto":false},{"name":"next_action_date","type":"DATE","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"},{"type":"belongsTo","entity":"customers","via":"customer_id"}]},{"name":"Dashboard","table":"dashboard","description":"ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜่กจ","fields":[{"name":"id","type":"UUID","required":true,"isPrimary":true,"isAuto":true},{"name":"user_id","type":"UUID","required":false,"isPrimary":false,"isAuto":false,"fkEntity":"users","fkColumn":"id"},{"name":"name","type":"VARCHAR(128)","required":true,"isPrimary":false,"isAuto":false},{"name":"description","type":"TEXT","required":false,"isPrimary":false,"isAuto":false},{"name":"color","type":"VARCHAR(7)","required":false,"isPrimary":false,"isAuto":false},{"name":"created_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true},{"name":"updated_at","type":"TIMESTAMPTZ","required":false,"isPrimary":false,"isAuto":true}],"relationships":[{"type":"belongsTo","entity":"users","via":"user_id"}]}],"auth":{"registrationFields":["username","password","nickname"],"loginFields":["username","password"],"jwtPayload":["userId","username","role"],"passwordPolicy":{"minLength":6,"requireSpecial":false}},"permissions":[{"role":"admin","allow":["*"]},{"role":"user","allow":["read:own","create:*","update:own","delete:own"]}],"fieldMapping":{"snakeToCamel":{"created_at":"createdAt","updated_at":"updatedAt","user_id":"userId","password_hash":"passwordHash","avatar_url":"avatarUrl","customer_id":"customerId","expected_close":"expectedClose","next_action":"nextAction","next_action_date":"nextActionDate","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","party_a":"partyA","party_b":"partyB","signed_at":"signedAt","expires_at":"expiresAt","file_url":"fileUrl","entity_type":"entityType","entity_id":"entityId","applicant_id":"applicantId","form_data":"formData","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birth_date":"birthDate","weight_kg":"weightKg","price":"price","stock":"stock","images":"images","total_amount":"totalAmount","address_id":"addressId","remind_at":"remindAt","message":"message","sent":"sent"},"camelToSnake":{"createdAt":"created_at","updatedAt":"updated_at","userId":"user_id","passwordHash":"password_hash","avatarUrl":"avatar_url","customerId":"customer_id","expectedClose":"expected_close","nextAction":"next_action","nextActionDate":"next_action_date","id":"id","username":"username","nickname":"nickname","role":"role","phone":"phone","title":"title","description":"description","status":"status","data":"data","amount":"amount","partyA":"party_a","partyB":"party_b","signedAt":"signed_at","expiresAt":"expires_at","fileUrl":"file_url","entityType":"entity_type","entityId":"entity_id","applicantId":"applicant_id","formData":"form_data","name":"name","email":"email","company":"company","source":"source","tags":"tags","breed":"breed","species":"species","birthDate":"birth_date","weightKg":"weight_kg","price":"price","stock":"stock","images":"images","totalAmount":"total_amount","addressId":"address_id","remindAt":"remind_at","message":"message","sent":"sent"}},"validation":{"User":{"username":{"minLength":2,"maxLength":50,"required":true,"unique":true},"password_hash":{"required":true},"role":{"enum":["user","admin"],"defaultValue":"user"}},"Customers":{"id":{"required":true},"name":{"required":true}},"SalesPipeline":{"id":{"required":true},"stage":{"required":true}},"FollowUps":{"id":{"required":true}},"Dashboard":{"id":{"required":true},"name":{"required":true}}}},"techStack":{"frontend":"React Native 0.76 + Expo / Flutter 3.x","backend":"NestJS + Prisma","database":"PostgreSQL 15 + Redis 7 + MinIO๏ผˆๆ–‡ไปถๅญ˜ๅ‚จ๏ผ‰","deployment":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","considerations":[]},"architectureDiagram":"โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ NoteApp โ€” System Architecture โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Client Layer โ”‚ โ”‚ Admin / Web โ”‚ โ”‚\nโ”‚ โ”‚ React Native 0.76 + Expo / Flutter 3.x โ”‚ โ”‚ React + Vite (Web) โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ API Gateway โ”‚ โ”‚\nโ”‚ โ”‚ Nginx / Kong โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ Backend Services โ”‚ โ”‚\nโ”‚ โ”‚ NestJS + Prisma โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ โ”‚ โ”‚ โ”‚\nโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚\nโ”‚ โ”‚ PostgreSQL 15โ”‚ โ”‚ Redis Cache โ”‚ โ”‚ MinIO โ”‚ โ”‚\nโ”‚ โ”‚ Primary โ”‚ โ”‚ Session โ”‚ โ”‚ Files โ”‚ โ”‚\nโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚\nโ”‚ โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ Modules: โ”‚\nโ”‚ 1 ๅฎขๆˆท็ฎก็† โ”‚\nโ”‚ 2 ้”€ๅ”ฎๆผๆ–— โ”‚\nโ”‚ 3 ่ทŸ่ฟ›่ฎฐๅฝ• โ”‚\nโ”‚ 4 Analytics โ”‚\nโ”‚ 5 ็ผ–่พ‘ๅ™จ โ”‚\nโ”‚ 6 ๆœ็ดขๅผ•ๆ“Ž โ”‚\nโ”‚ โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜","dataFlows":[{"page":"ๅฎขๆˆท็ฎก็†","route":"/customers","description":"ๅฎขๆˆท็ฎก็†","dataFlow":["GET /api/customers โ†’ ่Žทๅ–ๅฎขๆˆท็ฎก็†ๅˆ—่กจ","POST /api/customers โ†’ ๅˆ›ๅปบๅฎขๆˆท็ฎก็†"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"้”€ๅ”ฎๆผๆ–—","route":"/sales_pipeline","description":"้”€ๅ”ฎๆผๆ–—","dataFlow":["GET /api/sales_pipeline โ†’ ่Žทๅ–้”€ๅ”ฎๆผๆ–—ๅˆ—่กจ","POST /api/sales_pipeline โ†’ ๅˆ›ๅปบ้”€ๅ”ฎๆผๆ–—"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"่ทŸ่ฟ›่ฎฐๅฝ•","route":"/follow_ups","description":"่ทŸ่ฟ›่ฎฐๅฝ•","dataFlow":["GET /api/follow_ups โ†’ ่Žทๅ–่ทŸ่ฟ›่ฎฐๅฝ•ๅˆ—่กจ","POST /api/follow_ups โ†’ ๅˆ›ๅปบ่ทŸ่ฟ›่ฎฐๅฝ•"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"},{"page":"ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜","route":"/dashboard","description":"ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜","dataFlow":["GET /api/dashboard โ†’ ่Žทๅ–ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜ๅˆ—่กจ","POST /api/dashboard โ†’ ๅˆ›ๅปบๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜"],"direction":"Client โ†’ API Gateway โ†’ Backend โ†’ Database โ†’ Response โ†’ Client"}],"modules":[{"name":"ๅฎขๆˆท็ฎก็†","label":"ๅฎขๆˆท็ฎก็†","features":["ๅฎขๆˆท็ฎก็†"],"responsibilities":["ๆไพ› ๅฎขๆˆท็ฎก็† ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"้”€ๅ”ฎๆผๆ–—","label":"้”€ๅ”ฎๆผๆ–—","features":["้”€ๅ”ฎๆผๆ–—"],"responsibilities":["ๆไพ› ้”€ๅ”ฎๆผๆ–— ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"่ทŸ่ฟ›่ฎฐๅฝ•","label":"่ทŸ่ฟ›่ฎฐๅฝ•","features":["่ทŸ่ฟ›่ฎฐๅฝ•"],"responsibilities":["ๆไพ› ่ทŸ่ฟ›่ฎฐๅฝ• ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"analytics","label":"Analytics","features":["ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜"],"responsibilities":["ๆไพ› ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"editor","label":"็ผ–่พ‘ๅ™จ","features":["็ฌ”่ฎฐ็ผ–่พ‘"],"responsibilities":["ๆไพ› ็ฌ”่ฎฐ็ผ–่พ‘ ็›ธๅ…ณๅŠŸ่ƒฝ"]},{"name":"search","label":"ๆœ็ดขๅผ•ๆ“Ž","features":["ๅ…จๆ–‡ๆœ็ดข"],"responsibilities":["ๆไพ› ๅ…จๆ–‡ๆœ็ดข ็›ธๅ…ณๅŠŸ่ƒฝ"]}],"databaseSchema":[{"table":"users","description":"็”จๆˆท่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"phone","type":"VARCHAR(20)","constraints":"UNIQUE"},{"name":"nickname","type":"VARCHAR(64)"},{"name":"avatar_url","type":"TEXT"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_users_phone ON users(phone)"]},{"table":"customers","description":"ๅฎขๆˆท็ฎก็†่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"email","type":"VARCHAR(256)"},{"name":"phone","type":"VARCHAR(20)"},{"name":"company","type":"VARCHAR(128)"},{"name":"source","type":"VARCHAR(64)"},{"name":"tags","type":"JSONB","constraints":"DEFAULT '[]'"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_customers_user ON customers(user_id)"]},{"table":"sales_pipeline","description":"้”€ๅ”ฎๆผๆ–—่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"customer_id","type":"UUID","constraints":"FK โ†’ customers.id"},{"name":"stage","type":"VARCHAR(32)","constraints":"NOT NULL"},{"name":"amount","type":"DECIMAL(12,2)"},{"name":"probability","type":"INTEGER"},{"name":"expected_close","type":"DATE"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_sales_pipeline_user ON sales_pipeline(user_id)"]},{"table":"follow_ups","description":"่ทŸ่ฟ›่ฎฐๅฝ•่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"customer_id","type":"UUID","constraints":"FK โ†’ customers.id"},{"name":"type","type":"VARCHAR(32)"},{"name":"content","type":"TEXT"},{"name":"next_action","type":"VARCHAR(256)"},{"name":"next_action_date","type":"DATE"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_follow_ups_user ON follow_ups(user_id)"]},{"table":"dashboard","description":"ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜่กจ","fields":[{"name":"id","type":"UUID","constraints":"PK, DEFAULT gen_random_uuid()"},{"name":"user_id","type":"UUID","constraints":"FK โ†’ users.id"},{"name":"name","type":"VARCHAR(128)","constraints":"NOT NULL"},{"name":"description","type":"TEXT"},{"name":"color","type":"VARCHAR(7)"},{"name":"created_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"},{"name":"updated_at","type":"TIMESTAMPTZ","constraints":"DEFAULT NOW()"}],"indexes":["idx_dashboard_user ON dashboard(user_id)"]}],"apiDesign":[{"resource":"auth","basePath":"/api/auth","endpoints":[{"method":"POST","path":"/api/auth/login","description":"็”จๆˆท็™ปๅฝ•"},{"method":"GET","path":"/api/auth/me","description":"่Žทๅ–ๅฝ“ๅ‰็”จๆˆท"}]},{"resource":"customers","basePath":"/api/customers","endpoints":[{"method":"GET","path":"/api/customers","description":"่Žทๅ–ๅฎขๆˆท็ฎก็†ๅˆ—่กจ"},{"method":"POST","path":"/api/customers","description":"ๅˆ›ๅปบๅฎขๆˆท็ฎก็†"}]},{"resource":"dashboard","basePath":"/api/dashboard","endpoints":[{"method":"GET","path":"/api/dashboard","description":"่Žทๅ–ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜ๅˆ—่กจ"},{"method":"POST","path":"/api/dashboard","description":"ๅˆ›ๅปบๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜"}]},{"resource":"follow_ups","basePath":"/api/follow_ups","endpoints":[{"method":"GET","path":"/api/follow_ups","description":"่Žทๅ–่ทŸ่ฟ›่ฎฐๅฝ•ๅˆ—่กจ"},{"method":"POST","path":"/api/follow_ups","description":"ๅˆ›ๅปบ่ทŸ่ฟ›่ฎฐๅฝ•"}]},{"resource":"sales_pipeline","basePath":"/api/sales_pipeline","endpoints":[{"method":"GET","path":"/api/sales_pipeline","description":"่Žทๅ–้”€ๅ”ฎๆผๆ–—ๅˆ—่กจ"},{"method":"POST","path":"/api/sales_pipeline","description":"ๅˆ›ๅปบ้”€ๅ”ฎๆผๆ–—"}]}],"directoryStructure":["noteapp/","โ”œโ”€โ”€ apps/","โ”‚ โ”œโ”€โ”€ mobile/ # React Native / Flutter ็งปๅŠจ็ซฏ","โ”‚ โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ screens/ # ้กต้ข็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้”€ๅ”ฎๆผๆ–—/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ทŸ่ฟ›่ฎฐๅฝ•/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # ้€š็”จ็ป„ไปถ","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # ่‡ชๅฎšไน‰ Hooks","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API ่ฐƒ็”จๅฑ‚","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ # ็Šถๆ€็ฎก็†","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # ๅทฅๅ…ทๅ‡ฝๆ•ฐ","โ”‚ โ”‚ โ”œโ”€โ”€ app.json","โ”‚ โ”‚ โ””โ”€โ”€ package.json","โ”‚ โ””โ”€โ”€ web/ # Web ็ฎก็†ๅŽๅฐ","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ pages/","โ”‚ โ”‚ โ”œโ”€โ”€ components/","โ”‚ โ”‚ โ””โ”€โ”€ layouts/","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ packages/","โ”‚ โ””โ”€โ”€ shared/ # ๅ…ฑไบซ็ฑปๅž‹/ๅธธ้‡","โ”œโ”€โ”€ server/ # NestJS ๅŽ็ซฏๆœๅŠก","โ”‚ โ”œโ”€โ”€ src/","โ”‚ โ”‚ โ”œโ”€โ”€ modules/ # ไธšๅŠกๆจกๅ—","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅฎขๆˆท็ฎก็†.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้”€ๅ”ฎๆผๆ–—/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้”€ๅ”ฎๆผๆ–—.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้”€ๅ”ฎๆผๆ–—.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ้”€ๅ”ฎๆผๆ–—.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ทŸ่ฟ›่ฎฐๅฝ•/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ทŸ่ฟ›่ฎฐๅฝ•.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ทŸ่ฟ›่ฎฐๅฝ•.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่ทŸ่ฟ›่ฎฐๅฝ•.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ analytics.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ editor/","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ editor.controller.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ editor.service.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ editor.module.ts","โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ dto/","โ”‚ โ”‚ โ”œโ”€โ”€ common/ # ้€š็”จ๏ผˆguards, filters, interceptors๏ผ‰","โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ # Prisma ORM","โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ schema.prisma","โ”‚ โ”‚ โ”œโ”€โ”€ config/ # ็Žฏๅขƒ้…็ฝฎ","โ”‚ โ”‚ โ””โ”€โ”€ main.ts","โ”‚ โ”œโ”€โ”€ test/","โ”‚ โ”œโ”€โ”€ Dockerfile","โ”‚ โ””โ”€โ”€ package.json","โ”œโ”€โ”€ docker-compose.yml","โ”œโ”€โ”€ .github/workflows/ # CI/CD","โ”‚ โ””โ”€โ”€ ci.yml","โ”œโ”€โ”€ .env.example","โ”œโ”€โ”€ README.md","โ””โ”€โ”€ turbo.json # Monorepo ้…็ฝฎ"],"deployment":{"environments":["development","staging","production"],"strategy":"Docker Compose + Nginx / K8s๏ผˆ่ง„ๆจกๅŒ–๏ผ‰","services":["API Server (NestJS)","PostgreSQL 15","Redis 7"],"ci":"GitHub Actions โ†’ Build โ†’ Test โ†’ Deploy"},"meta":{"generatedAt":"2026-06-05T22:08:34.079Z","sourceDomain":"note","moduleCount":6,"tableCount":5,"apiEndpointCount":10}} \ No newline at end of file diff --git a/.benchmark/crm/backend/.gitignore b/.benchmark/crm/backend/.gitignore new file mode 100644 index 0000000..73ddc83 --- /dev/null +++ b/.benchmark/crm/backend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +*.db +*.db-journal +*.db-wal diff --git a/.benchmark/crm/backend/README.md b/.benchmark/crm/backend/README.md new file mode 100644 index 0000000..00690e5 --- /dev/null +++ b/.benchmark/crm/backend/README.md @@ -0,0 +1,90 @@ +# NoteApp โ€” Backend API + +> ไธ€ๆฌพๆ”ฏๆŒๅคš็ซฏๅŒๆญฅใ€Markdown ็ผ–่พ‘ๅ’Œๆ ‡็ญพ็ฎก็†็š„็ฌ”่ฎฐๅบ”็”จใ€‚ + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Fastify 5 +- **Language**: TypeScript +- **Database**: SQLite (better-sqlite3) +- **Auth**: JWT + bcrypt + +## Getting Started + +```bash +# Install dependencies +npm install + +# Development (hot reload) +npm run dev + +# Build +npm run build + +# Production start +npm run start + +# Run tests +npm test +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ index.ts # Server entry point +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ schema.ts # SQLite schema +โ”‚ โ””โ”€โ”€ client.ts # Database client +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.ts # Auth routes (register/login/me) +โ”‚ โ””โ”€โ”€ *.ts # CRUD routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ *.ts # Business logic +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.ts # JWT middleware +โ”œโ”€โ”€ types/ +โ”‚ โ””โ”€โ”€ index.ts # TypeScript types +โ””โ”€โ”€ __tests__/ + โ””โ”€โ”€ *.test.ts # Tests +``` + +## API Endpoints + +### Auth +- `POST /api/auth/register` โ€” Register +- `POST /api/auth/login` โ€” Login +- `GET /api/auth/me` โ€” Current user (auth required) + +### Resources +#### customers +- `GET /api/customers` โ€” List all +- `GET /api/customers/:id` โ€” Get by ID +- `POST /api/customers` โ€” Create +- `PUT /api/customers/:id` โ€” Update +- `DELETE /api/customers/:id` โ€” Delete + +#### sales_pipeline +- `GET /api/sales_pipeline` โ€” List all +- `GET /api/sales_pipeline/:id` โ€” Get by ID +- `POST /api/sales_pipeline` โ€” Create +- `PUT /api/sales_pipeline/:id` โ€” Update +- `DELETE /api/sales_pipeline/:id` โ€” Delete + +#### follow_ups +- `GET /api/follow_ups` โ€” List all +- `GET /api/follow_ups/:id` โ€” Get by ID +- `POST /api/follow_ups` โ€” Create +- `PUT /api/follow_ups/:id` โ€” Update +- `DELETE /api/follow_ups/:id` โ€” Delete + +#### dashboard +- `GET /api/dashboard` โ€” List all +- `GET /api/dashboard/:id` โ€” Get by ID +- `POST /api/dashboard` โ€” Create +- `PUT /api/dashboard/:id` โ€” Update +- `DELETE /api/dashboard/:id` โ€” Delete + +### System +- `GET /api/health` โ€” Health check diff --git a/.benchmark/crm/backend/package.json b/.benchmark/crm/backend/package.json new file mode 100644 index 0000000..6fa3b11 --- /dev/null +++ b/.benchmark/crm/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "noteapp", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node --import tsx --test src/__tests__/*.test.ts", + "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts" + }, + "dependencies": { + "fastify": "^5.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/jwt": "^9.0.0", + "sql.js": "^1.12.0", + "bcrypt": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/bcrypt": "^5.0.0", + "typescript": "^5.6.0", + "tsx": "^4.0.0", + "pino-pretty": "^11.0.0" + } +} \ No newline at end of file diff --git a/.benchmark/crm/backend/src/__tests__/auth.test.ts b/.benchmark/crm/backend/src/__tests__/auth.test.ts new file mode 100644 index 0000000..fcbf0c1 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/auth.test.ts @@ -0,0 +1,96 @@ +// Auth routes test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); +}); + +after(async () => { + await app.close(); +}); + +describe("POST /api/auth/register", () => { + it("registers a new user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.token); + assert.equal(body.user.username, "testuser"); + token = body.token; + }); + + it("rejects duplicate username", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 409); + }); + + it("rejects short password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: "user2", password: "123" }, + }); + assert.equal(res.statusCode, 400); + }); +}); + +describe("POST /api/auth/login", () => { + it("logs in with correct credentials", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "password123" }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(body.token); + token = body.token; + }); + + it("rejects wrong password", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username: "testuser", password: "wrongpassword" }, + }); + assert.equal(res.statusCode, 401); + }); +}); + +describe("GET /api/auth/me", () => { + it("returns current user with valid token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.username, "testuser"); + }); + + it("rejects without token", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/auth/me", + }); + assert.equal(res.statusCode, 401); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/customers.test.ts b/.benchmark/crm/backend/src/__tests__/customers.test.ts new file mode 100644 index 0000000..8425a02 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/customers.test.ts @@ -0,0 +1,121 @@ +// Customers CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "email": "sample-email", + "phone": "sample-phone", + "company": "sample-company", + "source": "sample-source" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/customers", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/customers", () => { + it("creates a customer", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/customers", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/customers/:id", () => { + it("returns the created customer", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/customers/:id", () => { + it("updates the customer", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/customers/:id", () => { + it("deletes the customer", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/customers/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/dashboard.test.ts b/.benchmark/crm/backend/src/__tests__/dashboard.test.ts new file mode 100644 index 0000000..4199529 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/dashboard.test.ts @@ -0,0 +1,119 @@ +// Dashboard CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "name": "sample-name", + "description": "sample-description", + "color": "sample-color" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/dashboard", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/dashboard", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/dashboard", () => { + it("creates a dashboard", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/dashboard", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/dashboard/:id", () => { + it("returns the created dashboard", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/dashboard/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/dashboard/:id", () => { + it("updates the dashboard", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/dashboard/:id", () => { + it("deletes the dashboard", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/dashboard/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/follow_ups.test.ts b/.benchmark/crm/backend/src/__tests__/follow_ups.test.ts new file mode 100644 index 0000000..de5e1ef --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/follow_ups.test.ts @@ -0,0 +1,121 @@ +// FollowUps CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "customerId": "sample-customerid", + "type": "sample-type", + "content": "sample-content", + "nextAction": "sample-nextaction", + "nextActionDate": "sample-nextactiondate" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/follow_ups", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/follow_ups", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/follow_ups", () => { + it("creates a follow_up", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/follow_ups", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/follow_ups/:id", () => { + it("returns the created follow_up", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/follow_ups/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/follow_ups/:id", () => { + it("updates the follow_up", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/follow_ups/:id", () => { + it("deletes the follow_up", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/follow_ups/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/note_tags.test.ts b/.benchmark/crm/backend/src/__tests__/note_tags.test.ts new file mode 100644 index 0000000..8544923 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/note_tags.test.ts @@ -0,0 +1,118 @@ +// NoteTag CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/note_tags", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/note_tags", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/note_tags", () => { + it("creates a note_tag", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/note_tags", + headers: { authorization: `Bearer ${token}` }, + payload: { + "noteId": "00000000-0000-0000-0000-000000000001", + "tagId": "00000000-0000-0000-0000-000000000001", + "PRIMARY KEY (noteId, tagId)": "sample-primary key (noteid, tagid)" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/note_tags/:id", () => { + it("returns the created note_tag", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/note_tags/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/note_tags/:id", () => { + it("updates the note_tag", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "noteId": "00000000-0000-0000-0000-000000000001", + "tagId": "00000000-0000-0000-0000-000000000001", + "PRIMARY KEY (noteId, tagId)": "sample-primary key (noteid, tagid)" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/note_tags/:id", () => { + it("deletes the note_tag", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/note_tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/notes.test.ts b/.benchmark/crm/backend/src/__tests__/notes.test.ts new file mode 100644 index 0000000..4e329a7 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/notes.test.ts @@ -0,0 +1,122 @@ +// Note CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/notes", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/notes", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/notes", () => { + it("creates a note", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/notes", + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "content": "sample-content", + "isMarkdown": true, + "folderId": "00000000-0000-0000-0000-000000000001" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/notes/:id", () => { + it("returns the created note", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/notes/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/notes/:id", () => { + it("updates the note", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "userId": "00000000-0000-0000-0000-000000000001", + "title": "sample-title", + "content": "sample-content", + "isMarkdown": true, + "folderId": "00000000-0000-0000-0000-000000000001" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/notes/:id", () => { + it("deletes the note", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/notes/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts b/.benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts new file mode 100644 index 0000000..9bd83c7 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/sales_pipeline.test.ts @@ -0,0 +1,121 @@ +// SalesPipeline CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; +let payload: Record; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; + + // Resolve user FK references with the registered user's real ID + payload = { + "userId": regRes.json().user.id, + "customerId": "sample-customerid", + "stage": "sample-stage", + "amount": 1, + "probability": 1, + "expectedClose": "sample-expectedclose" + }; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/sales_pipeline", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/sales_pipeline", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/sales_pipeline", () => { + it("creates a sales_pipeline", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/sales_pipeline", + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/sales_pipeline/:id", () => { + it("returns the created sales_pipeline", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/sales_pipeline/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/sales_pipeline/:id", () => { + it("updates the sales_pipeline", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/sales_pipeline/:id", () => { + it("deletes the sales_pipeline", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/sales_pipeline/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/tags.test.ts b/.benchmark/crm/backend/src/__tests__/tags.test.ts new file mode 100644 index 0000000..8505912 --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/tags.test.ts @@ -0,0 +1,116 @@ +// Tag CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/tags", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/tags", () => { + it("creates a tag", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/tags", + headers: { authorization: `Bearer ${token}` }, + payload: { + "name": "sample-name", + "color": "sample-color" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/tags/:id", () => { + it("returns the created tag", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/tags/:id", () => { + it("updates the tag", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "name": "sample-name", + "color": "sample-color" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/tags/:id", () => { + it("deletes the tag", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/tags/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/__tests__/users.test.ts b/.benchmark/crm/backend/src/__tests__/users.test.ts new file mode 100644 index 0000000..af7f85a --- /dev/null +++ b/.benchmark/crm/backend/src/__tests__/users.test.ts @@ -0,0 +1,118 @@ +// User CRUD test +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { buildApp } from "../index.js"; +import type { FastifyInstance } from "fastify"; + +let app: FastifyInstance; +let token: string; +let createdId: string; + +before(async () => { + process.env.JWT_SECRET = "test-secret"; + process.env.DATABASE_URL = ":memory:"; + app = await buildApp(); + await app.ready(); + + // Register and login to get a token + const regRes = await app.inject({ + method: "POST", + url: "/api/auth/register", + payload: { username: `test_${Date.now()}`, password: "password123" }, + }); + token = regRes.json().token; +}); + +after(async () => { + await app.close(); +}); + +describe("GET /api/users", () => { + it("returns empty list", async () => { + const res = await app.inject({ + method: "GET", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.ok(Array.isArray(body.data)); + }); +}); + +describe("POST /api/users", () => { + it("creates a user", async () => { + const res = await app.inject({ + method: "POST", + url: "/api/users", + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 201); + const body = res.json(); + assert.ok(body.data.id); + createdId = body.data.id; + }); +}); + +describe("GET /api/users/:id", () => { + it("returns the created user", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 200); + const body = res.json(); + assert.equal(body.data.id, createdId); + }); + + it("returns 404 for nonexistent id", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/nonexistent`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); + +describe("PUT /api/users/:id", () => { + it("updates the user", async () => { + const res = await app.inject({ + method: "PUT", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + payload: { + "phone": "sample-phone", + "nickname": "sample-nickname", + "avatarUrl": "sample-avatarurl" + }, + }); + assert.equal(res.statusCode, 200); + }); +}); + +describe("DELETE /api/users/:id", () => { + it("deletes the user", async () => { + const res = await app.inject({ + method: "DELETE", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 204); + }); + + it("returns 404 after delete", async () => { + const res = await app.inject({ + method: "GET", + url: `/api/users/${createdId}`, + headers: { authorization: `Bearer ${token}` }, + }); + assert.equal(res.statusCode, 404); + }); +}); diff --git a/.benchmark/crm/backend/src/db/client.ts b/.benchmark/crm/backend/src/db/client.ts new file mode 100644 index 0000000..d40be81 --- /dev/null +++ b/.benchmark/crm/backend/src/db/client.ts @@ -0,0 +1,93 @@ +// SQLite database client (sql.js โ€” pure WASM, no native deps) +import initSqlJs, { type Database, type BindParams } from "sql.js"; +import { createTables } from "./schema.js"; + +let db: Database | null = null; +let initPromise: Promise | null = null; + +/** Initialize the database (call once at startup). */ +export async function initDb(dbPath?: string): Promise { + if (db) return db; + if (initPromise) return initPromise; + + initPromise = (async () => { + const SQL = await initSqlJs(); + const path = dbPath || process.env.DATABASE_URL || ":memory:"; + + // Try to load existing database from file + let buffer: ArrayLike | undefined; + if (path !== ":memory:") { + try { + const fs = await import("node:fs/promises"); + const data = await fs.readFile(path); + buffer = new Uint8Array(data); + } catch { + // File doesn't exist yet โ€” start fresh + } + } + + db = new SQL.Database(buffer); + db.run("PRAGMA foreign_keys = ON"); + createTables(db); + return db; + })(); + + return initPromise; +} + +/** Get the initialized database (must call initDb first). */ +export function getDb(): Database { + if (!db) throw new Error("Database not initialized. Call initDb() first."); + return db; +} + +/** Save database to disk. */ +export async function saveDb(dbPath?: string): Promise { + if (!db) return; + const path = dbPath || process.env.DATABASE_URL || "./data/app.db"; + if (path === ":memory:") return; + const fs = await import("node:fs/promises"); + const { dirname } = await import("node:path"); + await fs.mkdir(dirname(path), { recursive: true }); + const data = db.export(); + await fs.writeFile(path, Buffer.from(data)); +} + +export async function closeDb(): Promise { + if (db) { + await saveDb(); + db.close(); + db = null; + initPromise = null; + } +} + +// Helper: run a query and return all rows as objects +export function queryAll>(sql: string, params: BindParams = []): T[] { + const d = getDb(); + const stmt = d.prepare(sql); + if (params) stmt.bind(params); + const results: T[] = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + results.push(row as unknown as T); + } + stmt.free(); + return results; +} + +// Helper: run a query and return the first row +export function queryOne>(sql: string, params: BindParams = []): T | undefined { + const rows = queryAll(sql, params); + return rows[0]; +} + +// Helper: run a mutation and return { changes, lastInsertRowid } +export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } { + const d = getDb(); + d.run(sql, params); + return { + changes: d.getRowsModified(), + lastInsertRowid: 0, + }; +} diff --git a/.benchmark/crm/backend/src/db/schema.ts b/.benchmark/crm/backend/src/db/schema.ts new file mode 100644 index 0000000..2bebc78 --- /dev/null +++ b/.benchmark/crm/backend/src/db/schema.ts @@ -0,0 +1,16 @@ +// Auto-generated SQLite schema +import type { Database } from "sql.js"; + +export function createTables(db: Database): void { + const statements = [ + "CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS customers (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n email TEXT,\n phone TEXT,\n company TEXT,\n source TEXT,\n tags TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS sales_pipeline (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n customer_id TEXT REFERENCES customers(id),\n stage TEXT NOT NULL,\n amount REAL,\n probability INTEGER,\n expected_close TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS follow_ups (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n customer_id TEXT REFERENCES customers(id),\n type TEXT,\n content TEXT,\n next_action TEXT,\n next_action_date TEXT,\n created_at TEXT,\n updated_at TEXT\n);", + "CREATE TABLE IF NOT EXISTS dashboard (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n name TEXT NOT NULL,\n description TEXT,\n color TEXT,\n created_at TEXT,\n updated_at TEXT\n);" + ]; + for (const sql of statements) { + const trimmed = sql.trim(); + if (trimmed) db.run(trimmed); + } +} diff --git a/.benchmark/crm/backend/src/index.ts b/.benchmark/crm/backend/src/index.ts new file mode 100644 index 0000000..b45ebd8 --- /dev/null +++ b/.benchmark/crm/backend/src/index.ts @@ -0,0 +1,71 @@ +// NoteApp โ€” Fastify Backend Server +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import fjwt from "@fastify/jwt"; +import { initDb, closeDb } from "./db/client.js"; +import { authRoutes } from "./routes/auth.js"; +import { customersRoutes } from "./routes/customers.js"; +import { sales_pipelineRoutes } from "./routes/sales_pipeline.js"; +import { follow_upsRoutes } from "./routes/follow_ups.js"; +import { dashboardRoutes } from "./routes/dashboard.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-80f3b14d"; + +export async function buildApp() { + const app = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: process.env.NODE_ENV !== "production" + ? { target: "pino-pretty", options: { colorize: true } } + : undefined, + }, + }); + + // Init database + await initDb(); + + // Plugins + await app.register(cors, { + origin: process.env.CORS_ORIGIN || "*", + methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + }); + + await app.register(fjwt, { secret: JWT_SECRET }); + + // Routes + await app.register(authRoutes, { prefix: "/api/auth" }); + await app.register(customersRoutes, { prefix: "/api/customers" }); + await app.register(sales_pipelineRoutes, { prefix: "/api/sales_pipeline" }); + await app.register(follow_upsRoutes, { prefix: "/api/follow_ups" }); + await app.register(dashboardRoutes, { prefix: "/api/dashboard" }); + + // Health check + app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); + + // Graceful shutdown + app.addHook("onClose", async () => { + closeDb(); + }); + + return app; +} + +// Start server if called directly (not when imported by tests) +const port = parseInt(process.env.PORT || "3001", 10); +const host = process.env.HOST || "0.0.0.0"; + +async function main() { + const app = await buildApp(); + try { + await app.listen({ port, host }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +// Guard: only run when executed directly, not when imported +const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js")); +if (isMain) { + main(); +} diff --git a/.benchmark/crm/backend/src/middleware/auth.ts b/.benchmark/crm/backend/src/middleware/auth.ts new file mode 100644 index 0000000..962692f --- /dev/null +++ b/.benchmark/crm/backend/src/middleware/auth.ts @@ -0,0 +1,41 @@ +// JWT Authentication Middleware +import type { FastifyRequest, FastifyReply } from "fastify"; +import type { JwtPayload } from "../types/index.js"; + +/** + * Verify JWT token and attach user to request. + */ +export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise { + try { + await request.jwtVerify(); + } catch (err) { + reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 }); + } +} + +/** Helper to get typed user from request (after authenticate). */ +export function getUser(request: FastifyRequest): JwtPayload { + return request.user as unknown as JwtPayload; +} + +/** + * Require admin role. + * Must be used after authenticate. + */ +export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise { + const user = request.user as unknown as JwtPayload | undefined; + if (!user || user.role !== "admin") { + reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 }); + } +} + +/** + * Optional auth: attach user if token present, but don't fail if missing. + */ +export async function optionalAuth(request: FastifyRequest): Promise { + try { + await request.jwtVerify(); + } catch { + // No token or invalid โ€” continue without user + } +} diff --git a/.benchmark/crm/backend/src/routes/auth.ts b/.benchmark/crm/backend/src/routes/auth.ts new file mode 100644 index 0000000..1518f8c --- /dev/null +++ b/.benchmark/crm/backend/src/routes/auth.ts @@ -0,0 +1,121 @@ +// Authentication routes +import type { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import { queryOne, execute } from "../db/client.js"; +import { authenticate } from "../middleware/auth.js"; +import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js"; + +const SALT_ROUNDS = 10; + +export async function authRoutes(app: FastifyInstance): Promise { + // POST /api/auth/register + app.post<{ Body: RegisterInput }>("/register", async (request, reply) => { + const { username, password, nickname } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + if (password.length < 6) { + return reply.status(400).send({ + error: "Bad Request", + message: "Password must be at least 6 characters", + statusCode: 400, + }); + } + + const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]); + if (existing) { + return reply.status(409).send({ + error: "Conflict", + message: "Username already exists", + statusCode: 409, + }); + } + + const id = crypto.randomUUID(); + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + const now = new Date().toISOString(); + + execute( + "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", + [id, username, passwordHash, nickname || username, now, now] + ); + + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [id] + ); + if (!user) { + return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 }); + } + + const token = app.jwt.sign({ userId: id, username, role: user.role }); + + return reply.status(201).send({ token, user } satisfies AuthResponse); + }); + + // POST /api/auth/login + app.post<{ Body: LoginInput }>("/login", async (request, reply) => { + const { username, password } = request.body; + + if (!username || !password) { + return reply.status(400).send({ + error: "Bad Request", + message: "Username and password are required", + statusCode: 400, + }); + } + + const user = queryOne( + "SELECT * FROM users WHERE username = ?", + [username] + ); + + if (!user) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return reply.status(401).send({ + error: "Unauthorized", + message: "Invalid username or password", + statusCode: 401, + }); + } + + const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role }); + + const { password_hash, ...safeUser } = user; + + return { token, user: safeUser } satisfies AuthResponse; + }); + + // GET /api/auth/me โ€” current user info + app.get("/me", { onRequest: [authenticate] }, async (request, reply) => { + const jwtUser = request.user as unknown as { userId: string }; + const user = queryOne( + "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?", + [jwtUser.userId] + ); + + if (!user) { + return reply.status(404).send({ + error: "Not Found", + message: "User not found", + statusCode: 404, + }); + } + + return { data: user }; + }); +} diff --git a/.benchmark/crm/backend/src/routes/customers.ts b/.benchmark/crm/backend/src/routes/customers.ts new file mode 100644 index 0000000..e22abcc --- /dev/null +++ b/.benchmark/crm/backend/src/routes/customers.ts @@ -0,0 +1,51 @@ +// Auto-generated Customers routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { CustomersService } from "../services/customer.js"; +import type { CreateCustomersInput, UpdateCustomersInput } from "../types/index.js"; + +const service = new CustomersService(); + +export async function customersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/customers โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/customers/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customers not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/customers โ€” create + app.post<{ Body: CreateCustomersInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/customers/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateCustomersInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Customers not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/customers/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Customers not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/dashboard.ts b/.benchmark/crm/backend/src/routes/dashboard.ts new file mode 100644 index 0000000..d7828c0 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/dashboard.ts @@ -0,0 +1,51 @@ +// Auto-generated Dashboard routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { DashboardService } from "../services/dashboard.js"; +import type { CreateDashboardInput, UpdateDashboardInput } from "../types/index.js"; + +const service = new DashboardService(); + +export async function dashboardRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/dashboard โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/dashboard/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Dashboard not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/dashboard โ€” create + app.post<{ Body: CreateDashboardInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/dashboard/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateDashboardInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Dashboard not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/dashboard/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Dashboard not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/follow_ups.ts b/.benchmark/crm/backend/src/routes/follow_ups.ts new file mode 100644 index 0000000..fabb3f8 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/follow_ups.ts @@ -0,0 +1,51 @@ +// Auto-generated FollowUps routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { FollowUpsService } from "../services/follow_up.js"; +import type { CreateFollowUpsInput, UpdateFollowUpsInput } from "../types/index.js"; + +const service = new FollowUpsService(); + +export async function follow_upsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/follow_ups โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/follow_ups/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "FollowUps not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/follow_ups โ€” create + app.post<{ Body: CreateFollowUpsInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/follow_ups/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateFollowUpsInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "FollowUps not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/follow_ups/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "FollowUps not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/note_tags.ts b/.benchmark/crm/backend/src/routes/note_tags.ts new file mode 100644 index 0000000..5faa25a --- /dev/null +++ b/.benchmark/crm/backend/src/routes/note_tags.ts @@ -0,0 +1,51 @@ +// Auto-generated NoteTag routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { NoteTagService } from "../services/note_tag.js"; +import type { CreateNoteTagInput, UpdateNoteTagInput } from "../types/index.js"; + +const service = new NoteTagService(); + +export async function note_tagsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/note_tags โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/note_tags/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/note_tags โ€” create + app.post<{ Body: CreateNoteTagInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/note_tags/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateNoteTagInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/note_tags/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/notes.ts b/.benchmark/crm/backend/src/routes/notes.ts new file mode 100644 index 0000000..db544f1 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/notes.ts @@ -0,0 +1,51 @@ +// Auto-generated Note routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { NoteService } from "../services/note.js"; +import type { CreateNoteInput, UpdateNoteInput } from "../types/index.js"; + +const service = new NoteService(); + +export async function notesRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/notes โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/notes/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/notes โ€” create + app.post<{ Body: CreateNoteInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/notes/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateNoteInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/notes/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/sales_pipeline.ts b/.benchmark/crm/backend/src/routes/sales_pipeline.ts new file mode 100644 index 0000000..4a5f3cf --- /dev/null +++ b/.benchmark/crm/backend/src/routes/sales_pipeline.ts @@ -0,0 +1,51 @@ +// Auto-generated SalesPipeline routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { SalesPipelineService } from "../services/sales_pipeline.js"; +import type { CreateSalesPipelineInput, UpdateSalesPipelineInput } from "../types/index.js"; + +const service = new SalesPipelineService(); + +export async function sales_pipelineRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/sales_pipeline โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/sales_pipeline/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "SalesPipeline not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/sales_pipeline โ€” create + app.post<{ Body: CreateSalesPipelineInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/sales_pipeline/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateSalesPipelineInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "SalesPipeline not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/sales_pipeline/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "SalesPipeline not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/tags.ts b/.benchmark/crm/backend/src/routes/tags.ts new file mode 100644 index 0000000..70cb8ba --- /dev/null +++ b/.benchmark/crm/backend/src/routes/tags.ts @@ -0,0 +1,51 @@ +// Auto-generated Tag routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { TagService } from "../services/tag.js"; +import type { CreateTagInput, UpdateTagInput } from "../types/index.js"; + +const service = new TagService(); + +export async function tagsRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/tags โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/tags/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/tags โ€” create + app.post<{ Body: CreateTagInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/tags/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateTagInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/tags/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/routes/users.ts b/.benchmark/crm/backend/src/routes/users.ts new file mode 100644 index 0000000..6235a18 --- /dev/null +++ b/.benchmark/crm/backend/src/routes/users.ts @@ -0,0 +1,51 @@ +// Auto-generated User routes +import type { FastifyInstance } from "fastify"; +import { authenticate } from "../middleware/auth.js"; +import { UserService } from "../services/user.js"; +import type { CreateUserInput, UpdateUserInput } from "../types/index.js"; + +const service = new UserService(); + +export async function usersRoutes(app: FastifyInstance): Promise { + // All routes require authentication + app.addHook("onRequest", authenticate); + + // GET /api/users โ€” list + app.get("/", async (request, reply) => { + const items = service.list(); + return { data: items }; + }); + + // GET /api/users/:id โ€” get by id + app.get<{ Params: { id: string } }>("/:id", async (request, reply) => { + const item = service.getById(request.params.id); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // POST /api/users โ€” create + app.post<{ Body: CreateUserInput }>("/", async (request, reply) => { + const item = service.create(request.body); + return reply.status(201).send({ data: item }); + }); + + // PUT /api/users/:id โ€” update + app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => { + const item = service.update(request.params.id, request.body); + if (!item) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return { data: item }; + }); + + // DELETE /api/users/:id โ€” delete + app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => { + const deleted = service.delete(request.params.id); + if (!deleted) { + return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 }); + } + return reply.status(204).send(); + }); +} diff --git a/.benchmark/crm/backend/src/services/customer.ts b/.benchmark/crm/backend/src/services/customer.ts new file mode 100644 index 0000000..fe775ea --- /dev/null +++ b/.benchmark/crm/backend/src/services/customer.ts @@ -0,0 +1,56 @@ +// Auto-generated Customers service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Customers, CreateCustomersInput, UpdateCustomersInput } from "../types/index.js"; + +export class CustomersService { + /** List all customers */ + list(): Customers[] { + return queryAll("SELECT * FROM customers ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Customers | undefined { + return queryOne("SELECT * FROM customers WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateCustomersInput): Customers { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "email", "phone", "company", "source", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.email ?? null, input.phone ?? null, input.company ?? null, input.source ?? null, now, now]; + + execute(`INSERT INTO customers (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateCustomersInput): Customers | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.email !== undefined) { sets.push("email = ?"); values.push(input.email); } + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.company !== undefined) { sets.push("company = ?"); values.push(input.company); } + if (input.source !== undefined) { sets.push("source = ?"); values.push(input.source); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM customers WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/dashboard.ts b/.benchmark/crm/backend/src/services/dashboard.ts new file mode 100644 index 0000000..6beae29 --- /dev/null +++ b/.benchmark/crm/backend/src/services/dashboard.ts @@ -0,0 +1,54 @@ +// Auto-generated Dashboard service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Dashboard, CreateDashboardInput, UpdateDashboardInput } from "../types/index.js"; + +export class DashboardService { + /** List all dashboard */ + list(): Dashboard[] { + return queryAll("SELECT * FROM dashboard ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Dashboard | undefined { + return queryOne("SELECT * FROM dashboard WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateDashboardInput): Dashboard { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "name", "description", "color", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.name ?? null, input.description ?? null, input.color ?? null, now, now]; + + execute(`INSERT INTO dashboard (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateDashboardInput): Dashboard | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); } + if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE dashboard SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM dashboard WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/follow_up.ts b/.benchmark/crm/backend/src/services/follow_up.ts new file mode 100644 index 0000000..e4e2715 --- /dev/null +++ b/.benchmark/crm/backend/src/services/follow_up.ts @@ -0,0 +1,56 @@ +// Auto-generated FollowUps service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { FollowUps, CreateFollowUpsInput, UpdateFollowUpsInput } from "../types/index.js"; + +export class FollowUpsService { + /** List all follow_ups */ + list(): FollowUps[] { + return queryAll("SELECT * FROM follow_ups ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): FollowUps | undefined { + return queryOne("SELECT * FROM follow_ups WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateFollowUpsInput): FollowUps { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "customer_id", "type", "content", "next_action", "next_action_date", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.customerId ?? null, input.type ?? null, input.content ?? null, input.nextAction ?? null, input.nextActionDate ?? null, now, now]; + + execute(`INSERT INTO follow_ups (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateFollowUpsInput): FollowUps | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.customerId !== undefined) { sets.push("customer_id = ?"); values.push(input.customerId); } + if (input.type !== undefined) { sets.push("type = ?"); values.push(input.type); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.nextAction !== undefined) { sets.push("next_action = ?"); values.push(input.nextAction); } + if (input.nextActionDate !== undefined) { sets.push("next_action_date = ?"); values.push(input.nextActionDate); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE follow_ups SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM follow_ups WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/note.ts b/.benchmark/crm/backend/src/services/note.ts new file mode 100644 index 0000000..a206fa0 --- /dev/null +++ b/.benchmark/crm/backend/src/services/note.ts @@ -0,0 +1,58 @@ +// Auto-generated Note service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Note, CreateNoteInput, UpdateNoteInput } from "../types/index.js"; + +export class NoteService { + /** List all notes */ + list(): Note[] { + return queryAll("SELECT * FROM notes ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Note | undefined { + return queryOne("SELECT * FROM notes WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateNoteInput): Note { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "user_id", "title", "content", "is_markdown", "folder_id", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.userId ?? null, input.title ?? null, input.content ?? null, input.isMarkdown ?? null, input.folderId ?? null, now, now]; + + execute(`INSERT INTO notes (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateNoteInput): Note | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); } + if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); } + if (input.isMarkdown !== undefined) { sets.push("is_markdown = ?"); values.push(input.isMarkdown); } + if (input.folderId !== undefined) { sets.push("folder_id = ?"); values.push(input.folderId); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE notes SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM notes WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/note_tag.ts b/.benchmark/crm/backend/src/services/note_tag.ts new file mode 100644 index 0000000..91a5652 --- /dev/null +++ b/.benchmark/crm/backend/src/services/note_tag.ts @@ -0,0 +1,54 @@ +// Auto-generated NoteTag service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { NoteTag, CreateNoteTagInput, UpdateNoteTagInput } from "../types/index.js"; + +export class NoteTagService { + /** List all note_tags */ + list(): NoteTag[] { + return queryAll("SELECT * FROM note_tags ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): NoteTag | undefined { + return queryOne("SELECT * FROM note_tags WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateNoteTagInput): NoteTag { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "note_id", "tag_id"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.noteId ?? null, input.tagId ?? null]; + + execute(`INSERT INTO note_tags (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateNoteTagInput): NoteTag | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.noteId !== undefined) { sets.push("note_id = ?"); values.push(input.noteId); } + if (input.tagId !== undefined) { sets.push("tag_id = ?"); values.push(input.tagId); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE note_tags SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM note_tags WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/sales_pipeline.ts b/.benchmark/crm/backend/src/services/sales_pipeline.ts new file mode 100644 index 0000000..543e5c8 --- /dev/null +++ b/.benchmark/crm/backend/src/services/sales_pipeline.ts @@ -0,0 +1,56 @@ +// Auto-generated SalesPipeline service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { SalesPipeline, CreateSalesPipelineInput, UpdateSalesPipelineInput } from "../types/index.js"; + +export class SalesPipelineService { + /** List all sales_pipeline */ + list(): SalesPipeline[] { + return queryAll("SELECT * FROM sales_pipeline ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): SalesPipeline | undefined { + return queryOne("SELECT * FROM sales_pipeline WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateSalesPipelineInput): SalesPipeline { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const cols = ["id", "user_id", "customer_id", "stage", "amount", "probability", "expected_close", "created_at", "updated_at"]; + const values = [id, input.userId ?? null, input.customerId ?? null, input.stage ?? null, input.amount ?? null, input.probability ?? null, input.expectedClose ?? null, now, now]; + + execute(`INSERT INTO sales_pipeline (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateSalesPipelineInput): SalesPipeline | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); } + if (input.customerId !== undefined) { sets.push("customer_id = ?"); values.push(input.customerId); } + if (input.stage !== undefined) { sets.push("stage = ?"); values.push(input.stage); } + if (input.amount !== undefined) { sets.push("amount = ?"); values.push(input.amount); } + if (input.probability !== undefined) { sets.push("probability = ?"); values.push(input.probability); } + if (input.expectedClose !== undefined) { sets.push("expected_close = ?"); values.push(input.expectedClose); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE sales_pipeline SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM sales_pipeline WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/tag.ts b/.benchmark/crm/backend/src/services/tag.ts new file mode 100644 index 0000000..267443d --- /dev/null +++ b/.benchmark/crm/backend/src/services/tag.ts @@ -0,0 +1,54 @@ +// Auto-generated Tag service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { Tag, CreateTagInput, UpdateTagInput } from "../types/index.js"; + +export class TagService { + /** List all tags */ + list(): Tag[] { + return queryAll("SELECT * FROM tags ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): Tag | undefined { + return queryOne("SELECT * FROM tags WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateTagInput): Tag { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = false; + const hasUpdatedAt = false; + const cols = ["id", "name", "color"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.name ?? null, input.color ?? null]; + + execute(`INSERT INTO tags (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateTagInput): Tag | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); } + if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); } + + if (sets.length === 0) return existing; + + + + values.push(id); + execute(`UPDATE tags SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM tags WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/services/user.ts b/.benchmark/crm/backend/src/services/user.ts new file mode 100644 index 0000000..02c1d9a --- /dev/null +++ b/.benchmark/crm/backend/src/services/user.ts @@ -0,0 +1,56 @@ +// Auto-generated User service +import { queryAll, queryOne, execute } from "../db/client.js"; +import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js"; + +export class UserService { + /** List all users */ + list(): User[] { + return queryAll("SELECT * FROM users ORDER BY created_at DESC"); + } + + /** Get by ID */ + getById(id: string): User | undefined { + return queryOne("SELECT * FROM users WHERE id = ?", [id]); + } + + /** Create */ + create(input: CreateUserInput): User { + const id = crypto.randomUUID(); + const now = new Date().toISOString(); + const hasCreatedAt = true; + const hasUpdatedAt = true; + const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"]; + const placeholders = cols.map(() => "?").join(", "); + const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now]; + + execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values); + return this.getById(id)!; + } + + /** Update */ + update(id: string, input: UpdateUserInput): User | undefined { + const existing = this.getById(id); + if (!existing) return undefined; + + const sets: string[] = []; + const values: unknown[] = []; + if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); } + if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); } + if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); } + + if (sets.length === 0) return existing; + + sets.push("updated_at = ?"); + values.push(new Date().toISOString()); + + values.push(id); + execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values); + return this.getById(id)!; + } + + /** Delete */ + delete(id: string): boolean { + const result = execute("DELETE FROM users WHERE id = ?", [id]); + return result.changes > 0; + } +} diff --git a/.benchmark/crm/backend/src/types/fastify.d.ts b/.benchmark/crm/backend/src/types/fastify.d.ts new file mode 100644 index 0000000..9e1c77b --- /dev/null +++ b/.benchmark/crm/backend/src/types/fastify.d.ts @@ -0,0 +1,8 @@ +// Augment Fastify request with JWT user +import type { JwtPayload } from "./index.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: JwtPayload; + } +} diff --git a/.benchmark/crm/backend/src/types/index.ts b/.benchmark/crm/backend/src/types/index.ts new file mode 100644 index 0000000..fe2dee4 --- /dev/null +++ b/.benchmark/crm/backend/src/types/index.ts @@ -0,0 +1,191 @@ +// Auto-generated types (from Model Contract) + +export interface User { + id?: string; + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Customers { + id: string; + userId?: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; + tags?: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface SalesPipeline { + id: string; + userId?: string; + customerId?: string; + stage: string; + amount?: number; + probability?: number; + expectedClose?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface FollowUps { + id: string; + userId?: string; + customerId?: string; + type?: string; + content?: string; + nextAction?: string; + nextActionDate?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Dashboard { + id: string; + userId?: string; + name: string; + description?: string; + color?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CreateUserInput { + username: string; + passwordHash: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface CreateCustomersInput { + userId?: string; + name: string; + email?: string; + phone?: string; + company?: string; + source?: string; +} + +export interface CreateSalesPipelineInput { + userId?: string; + customerId?: string; + stage: string; + amount?: number; + probability?: number; + expectedClose?: string; +} + +export interface CreateFollowUpsInput { + userId?: string; + customerId?: string; + type?: string; + content?: string; + nextAction?: string; + nextActionDate?: string; +} + +export interface CreateDashboardInput { + userId?: string; + name: string; + description?: string; + color?: string; +} + +export interface UpdateUserInput { + username?: string; + passwordHash?: string; + nickname?: string; + role?: string; + phone?: string; + avatarUrl?: string; +} + +export interface UpdateCustomersInput { + userId?: string; + name?: string; + email?: string; + phone?: string; + company?: string; + source?: string; +} + +export interface UpdateSalesPipelineInput { + userId?: string; + customerId?: string; + stage?: string; + amount?: number; + probability?: number; + expectedClose?: string; +} + +export interface UpdateFollowUpsInput { + userId?: string; + customerId?: string; + type?: string; + content?: string; + nextAction?: string; + nextActionDate?: string; +} + +export interface UpdateDashboardInput { + userId?: string; + name?: string; + description?: string; + color?: string; +} + +// โ”€โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface LoginInput { + username: string; + password: string; +} + +export interface RegisterInput { + username: string; + password: string; + nickname?: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +// โ”€โ”€โ”€ API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ApiResponse { + data: T; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +// โ”€โ”€โ”€ JWT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface JwtPayload { + userId: string; + username: string; + role: string; + iat?: number; + exp?: number; +} diff --git a/.benchmark/crm/backend/src/types/sql.js.d.ts b/.benchmark/crm/backend/src/types/sql.js.d.ts new file mode 100644 index 0000000..72aa934 --- /dev/null +++ b/.benchmark/crm/backend/src/types/sql.js.d.ts @@ -0,0 +1,27 @@ +// Type declarations for sql.js (no native types package available) +declare module "sql.js" { + export interface Database { + run(sql: string, params?: BindParams): void; + exec(sql: string): void; + prepare(sql: string): Statement; + export(): Uint8Array; + close(): void; + getRowsModified(): number; + } + + export interface Statement { + bind(params?: BindParams): boolean; + step(): boolean; + getAsObject>(): T; + getColumnNames(): string[]; + free(): boolean; + } + + export type BindParams = unknown[] | Record; + + export interface SqlJsStatic { + Database: new (data?: ArrayLike) => Database; + } + + export default function initSqlJs(config?: Record): Promise; +} diff --git a/.benchmark/crm/backend/tsconfig.json b/.benchmark/crm/backend/tsconfig.json new file mode 100644 index 0000000..e04d1de --- /dev/null +++ b/.benchmark/crm/backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} \ No newline at end of file diff --git a/.benchmark/crm/electron/README.md b/.benchmark/crm/electron/README.md new file mode 100644 index 0000000..4372420 --- /dev/null +++ b/.benchmark/crm/electron/README.md @@ -0,0 +1,82 @@ +# noteapp โ€” Desktop + +> Electron desktop application wrapping the noteapp fullstack web project. + +## Architecture + +``` +Web Fullstack Project + ร— + Desktop Shell (Electron) + = + Desktop Application +``` + +- **Main Process** โ€” Window management, menu, tray, IPC, auto-update +- **Preload** โ€” Secure bridge between main and renderer +- **Renderer** โ€” Your existing web frontend (Next.js) + +## Development + +```bash +# Install dependencies +npm install + +# Start dev mode (web + api + electron concurrently) +npm run dev +``` + +Dev mode: +- Web frontend: `http://localhost:3000` +- API backend: `http://localhost:3001` +- Electron loads the web URL directly + +## Build + +```bash +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:electron:mac +npm run build:electron:win +npm run build:electron:linux +``` + +Output: `release/` directory with platform installers. + +## Project Structure + +``` +electron/ +โ”œโ”€โ”€ main.ts โ€” Main process (BrowserWindow, Menu, lifecycle) +โ”œโ”€โ”€ preload.ts โ€” Secure IPC bridge (contextBridge) +โ”œโ”€โ”€ ipc.ts โ€” IPC handlers (dialogs, store, notifications) +โ”œโ”€โ”€ tray.ts โ€” System tray +โ”œโ”€โ”€ updater.ts โ€” Auto-update (electron-updater) +โ””โ”€โ”€ utils.ts โ€” Shared utilities +``` + +## IPC API (available in renderer) + +```typescript +window.electronAPI.getAppVersion() +window.electronAPI.getPlatform() +window.electronAPI.minimizeWindow() +window.electronAPI.maximizeWindow() +window.electronAPI.closeWindow() +window.electronAPI.openFile(options?) +window.electronAPI.saveFile(options?) +window.electronAPI.showNotification(title, body) +window.electronAPI.openExternal(url) +window.electronAPI.storeGet(key) +window.electronAPI.storeSet(key, value) +window.electronAPI.checkForUpdates() +window.electronAPI.downloadUpdate() +window.electronAPI.quitAndInstall() +``` + +## Auto Update + +Configured via `electron-builder.yml`. Uses `electron-updater` with generic provider. +Set your release URL in the `publish` section. diff --git a/.benchmark/crm/electron/assets/icon.png b/.benchmark/crm/electron/assets/icon.png new file mode 100644 index 0000000..554e8a3 Binary files /dev/null and b/.benchmark/crm/electron/assets/icon.png differ diff --git a/.benchmark/crm/electron/electron-builder.yml b/.benchmark/crm/electron/electron-builder.yml new file mode 100644 index 0000000..8f64fd1 --- /dev/null +++ b/.benchmark/crm/electron/electron-builder.yml @@ -0,0 +1,100 @@ +# electron-builder.yml โ€” Auto-generated by SF-06 + +appId: com.noteapp.desktop +productName: noteapp +copyright: "Copyright ยฉ 2026 noteapp" + +# โ”€โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +directories: + output: release + buildResources: assets + +# โ”€โ”€โ”€ Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +files: + - dist/electron/**/* + - "!node_modules/**/*" + - "**/*.node" + +# โ”€โ”€โ”€ Extra Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +extraResources: + - from: "../web/out" + to: "web" + filter: + - "**/*" + - from: "../api/dist" + to: "api" + filter: + - "**/*" + - from: "../api/data" + to: "data" + filter: + - "**/*" + +# โ”€โ”€โ”€ macOS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +mac: + category: public.app-category.productivity + icon: assets/icon.png + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + darkModeSupport: true + hardenedRuntime: true + gatekeeperAssess: false + entitlements: entitlements.mac.plist + entitlementsInherit: entitlements.mac.plist + +# โ”€โ”€โ”€ Windows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +win: + icon: assets/icon.png + target: + - target: nsis + arch: + - x64 + - target: portable + arch: + - x64 + publisherName: noteapp + +nsis: + oneClick: false + perMachine: false + allowToChangeInstallationDirectory: true + installerIcon: assets/icon.ico + uninstallerIcon: assets/icon.ico + installerHeaderIcon: assets/icon.ico + createDesktopShortcut: true + createStartMenuShortcut: true + shortcutName: noteapp + +# โ”€โ”€โ”€ Linux โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +linux: + icon: assets/icon.png + category: Utility + target: + - target: AppImage + arch: + - x64 + - target: deb + arch: + - x64 + - target: rpm + arch: + - x64 + desktop: + Name: noteapp + Comment: noteapp Desktop Application + Terminal: false + Type: Application + Categories: Utility; + +# โ”€โ”€โ”€ Auto Update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +publish: + provider: generic + url: https://releases.example.com/noteapp/ + channel: latest diff --git a/.benchmark/crm/electron/electron/ipc.ts b/.benchmark/crm/electron/electron/ipc.ts new file mode 100644 index 0000000..51a67b8 --- /dev/null +++ b/.benchmark/crm/electron/electron/ipc.ts @@ -0,0 +1,77 @@ +import { ipcMain, app, dialog, shell, BrowserWindow, Notification } from "electron"; +import Store from "electron-store"; + +const store = new Store() as any; + +export function setupIPC(mainWindow: BrowserWindow): void { + // โ”€โ”€ App Info โ”€โ”€ + ipcMain.handle("app:version", () => app.getVersion()); + ipcMain.handle("app:platform", () => process.platform); + ipcMain.handle("app:isDev", () => !app.isPackaged); + + // โ”€โ”€ Window Controls โ”€โ”€ + ipcMain.on("window:minimize", () => { + mainWindow?.minimize(); + }); + + ipcMain.on("window:maximize", () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } + }); + + ipcMain.on("window:close", () => { + mainWindow?.close(); + }); + + ipcMain.handle("window:isMaximized", () => { + return mainWindow?.isMaximized() ?? false; + }); + + // โ”€โ”€ File Dialogs โ”€โ”€ + ipcMain.handle("dialog:openFile", async (_event, options?) => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ["openFile"], + ...options, + }); + return result.canceled ? undefined : result.filePaths; + }); + + ipcMain.handle("dialog:saveFile", async (_event, options?) => { + const result = await dialog.showSaveDialog(mainWindow, { + ...options, + }); + return result.canceled ? undefined : result.filePath; + }); + + // โ”€โ”€ Notifications โ”€โ”€ + ipcMain.on("notification:show", (_event, { title, body }) => { + if (Notification.isSupported()) { + new Notification({ title, body }).show(); + } + }); + + // โ”€โ”€ Shell โ”€โ”€ + ipcMain.on("shell:openExternal", (_event, url: string) => { + shell.openExternal(url); + }); + + ipcMain.on("shell:openPath", (_event, path: string) => { + shell.openPath(path); + }); + + // โ”€โ”€ Persistent Store โ”€โ”€ + ipcMain.handle("store:get", (_event, key: string) => { + return store.get(key); + }); + + ipcMain.handle("store:set", (_event, key: string, value: unknown) => { + store.set(key, value); + }); + + ipcMain.handle("store:delete", (_event, key: string) => { + store.delete(key); + }); +} diff --git a/.benchmark/crm/electron/electron/main.ts b/.benchmark/crm/electron/electron/main.ts new file mode 100644 index 0000000..6f03c94 --- /dev/null +++ b/.benchmark/crm/electron/electron/main.ts @@ -0,0 +1,238 @@ +import { app, BrowserWindow, Menu, nativeImage, ipcMain } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; +import { setupTray } from "./tray"; +import { setupIPC } from "./ipc"; +import { setupUpdater } from "./updater"; +import { checkSingleInstance, getWebUrl, getApiPath, isDev } from "./utils"; + +// โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const WEB_PORT = 3000; +const API_PORT = 3001; +const APP_NAME = "noteapp"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if (!checkSingleInstance()) { + app.quit(); + process.exit(0); +} + +// โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +let mainWindow: BrowserWindow | null = null; +let apiProcess: ReturnType | null = null; + +// โ”€โ”€โ”€ Create Window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function createWindow(): BrowserWindow { + const preloadPath = join(__dirname, "preload.js"); + + const win = new BrowserWindow({ + title: APP_NAME, + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + show: false, + icon: getIconPath(), + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", + trafficLightPosition: { x: 16, y: 16 }, + }); + + // โ”€โ”€ Load content โ”€โ”€ + const url = getWebUrl(isDev(), WEB_PORT); + if (url.startsWith("http")) { + win.loadURL(url); + } else { + win.loadFile(url); + } + + // โ”€โ”€ Show when ready โ”€โ”€ + win.once("ready-to-show", () => { + win.show(); + if (isDev()) { + win.webContents.openDevTools({ mode: "detach" }); + } + }); + + // โ”€โ”€ External links โ†’ default browser โ”€โ”€ + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http")) { + require("electron").shell.openExternal(url); + } + return { action: "deny" }; + }); + + // โ”€โ”€ Prevent navigation away โ”€โ”€ + win.webContents.on("will-navigate", (event, url) => { + const allowed = isDev() + ? url.startsWith("http://localhost:3000") + : url.startsWith("file://"); + if (!allowed) event.preventDefault(); + }); + + return win; +} + +// โ”€โ”€โ”€ Icon Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getIconPath(): string { + const candidates = [ + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.icns"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return ""; +} + +// โ”€โ”€โ”€ API Server (Production) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function startApiServer(): void { + if (isDev()) return; + + const apiPath = getApiPath(); + if (!apiPath || !existsSync(apiPath)) { + console.warn("[main] API server not found at", apiPath); + return; + } + + const { spawn } = require("child_process"); + apiProcess = spawn("node", [apiPath], { + env: { + ...process.env, + NODE_ENV: "production", + PORT: String(API_PORT), + HOST: "127.0.0.1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + apiProcess!.stdout?.on("data", (data: Buffer) => { + console.log("[api]", data.toString().trim()); + }); + apiProcess!.stderr?.on("data", (data: Buffer) => { + console.error("[api]", data.toString().trim()); + }); + apiProcess!.on("exit", (code: number) => { + console.warn("[main] API server exited with code", code); + apiProcess = null; + }); +} + +function stopApiServer(): void { + if (apiProcess) { + apiProcess.kill("SIGTERM"); + apiProcess = null; + } +} + +// โ”€โ”€โ”€ Menu โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function buildMenu(): void { + const isMac = process.platform === "darwin"; + + const template: Electron.MenuItemConstructorOptions[] = [ + ...(isMac + ? [{ + label: APP_NAME, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "services" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// โ”€โ”€โ”€ App Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +app.whenReady().then(() => { + startApiServer(); + mainWindow = createWindow(); + buildMenu(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + setupUpdater(APP_NAME); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createWindow(); + if (mainWindow) { + setupTray(mainWindow, APP_NAME); + setupIPC(mainWindow); + } + } + }); +}); + +app.on("window-all-closed", () => { + stopApiServer(); + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("before-quit", () => { + stopApiServer(); +}); + +app.on("gpu-process-crashed" as any, () => { + console.warn("[main] GPU process crashed โ€” continuing"); +}); + +process.on("exit", () => { + stopApiServer(); +}); diff --git a/.benchmark/crm/electron/electron/preload.ts b/.benchmark/crm/electron/electron/preload.ts new file mode 100644 index 0000000..84bdda7 --- /dev/null +++ b/.benchmark/crm/electron/electron/preload.ts @@ -0,0 +1,92 @@ +import { contextBridge, ipcRenderer } from "electron"; + +// โ”€โ”€โ”€ Expose safe API to renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +contextBridge.exposeInMainWorld("electronAPI", { + // โ”€โ”€ App Info โ”€โ”€ + getAppVersion: () => ipcRenderer.invoke("app:version"), + getPlatform: () => ipcRenderer.invoke("app:platform"), + getIsDev: () => ipcRenderer.invoke("app:isDev"), + + // โ”€โ”€ Window Controls โ”€โ”€ + minimizeWindow: () => ipcRenderer.send("window:minimize"), + maximizeWindow: () => ipcRenderer.send("window:maximize"), + closeWindow: () => ipcRenderer.send("window:close"), + isMaximized: () => ipcRenderer.invoke("window:isMaximized"), + + // โ”€โ”€ File Dialogs โ”€โ”€ + openFile: (options?: Electron.OpenDialogOptions) => + ipcRenderer.invoke("dialog:openFile", options), + saveFile: (options?: Electron.SaveDialogOptions) => + ipcRenderer.invoke("dialog:saveFile", options), + + // โ”€โ”€ Notifications โ”€โ”€ + showNotification: (title: string, body: string) => + ipcRenderer.send("notification:show", { title, body }), + + // โ”€โ”€ Shell โ”€โ”€ + openExternal: (url: string) => ipcRenderer.send("shell:openExternal", url), + openPath: (path: string) => ipcRenderer.send("shell:openPath", path), + + // โ”€โ”€ Store โ”€โ”€ + storeGet: (key: string) => ipcRenderer.invoke("store:get", key), + storeSet: (key: string, value: unknown) => + ipcRenderer.invoke("store:set", key, value), + storeDelete: (key: string) => ipcRenderer.invoke("store:delete", key), + + // โ”€โ”€ Updates โ”€โ”€ + checkForUpdates: () => ipcRenderer.invoke("updater:check"), + downloadUpdate: () => ipcRenderer.invoke("updater:download"), + quitAndInstall: () => ipcRenderer.send("updater:quitAndInstall"), + + // โ”€โ”€ Update Events โ”€โ”€ + onUpdateAvailable: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:available", (_event, info) => callback(info)); + }, + onUpdateDownloaded: (callback: (info: unknown) => void) => { + ipcRenderer.on("updater:downloaded", (_event, info) => callback(info)); + }, + onUpdateProgress: (callback: (progress: unknown) => void) => { + ipcRenderer.on("updater:progress", (_event, progress) => callback(progress)); + }, + onUpdateError: (callback: (error: string) => void) => { + ipcRenderer.on("updater:error", (_event, error) => callback(error)); + }, + + // โ”€โ”€ Remove listener โ”€โ”€ + removeAllListeners: (channel: string) => { + ipcRenderer.removeAllListeners(channel); + }, +}); + +// โ”€โ”€โ”€ Type declarations for renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ElectronAPI { + getAppVersion: () => Promise; + getPlatform: () => Promise; + getIsDev: () => Promise; + minimizeWindow: () => void; + maximizeWindow: () => void; + closeWindow: () => void; + isMaximized: () => Promise; + openFile: (options?: Electron.OpenDialogOptions) => Promise; + saveFile: (options?: Electron.SaveDialogOptions) => Promise; + showNotification: (title: string, body: string) => void; + openExternal: (url: string) => void; + openPath: (path: string) => void; + storeGet: (key: string) => Promise; + storeSet: (key: string, value: unknown) => Promise; + storeDelete: (key: string) => Promise; + checkForUpdates: () => Promise; + downloadUpdate: () => Promise; + quitAndInstall: () => void; + onUpdateAvailable: (callback: (info: unknown) => void) => void; + onUpdateDownloaded: (callback: (info: unknown) => void) => void; + onUpdateProgress: (callback: (progress: unknown) => void) => void; + onUpdateError: (callback: (error: string) => void) => void; + removeAllListeners: (channel: string) => void; +} + +declare global { + interface Window { + electronAPI: ElectronAPI; + } +} diff --git a/.benchmark/crm/electron/electron/tray.ts b/.benchmark/crm/electron/electron/tray.ts new file mode 100644 index 0000000..3df65e3 --- /dev/null +++ b/.benchmark/crm/electron/electron/tray.ts @@ -0,0 +1,61 @@ +import { Tray, Menu, nativeImage, BrowserWindow, app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +let tray: Tray | null = null; + +export function setupTray(mainWindow: BrowserWindow, appName: string): void { + const iconPath = getTrayIcon(); + if (!iconPath) { + console.warn("[tray] No icon found, skipping tray setup"); + return; + } + + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + tray.setToolTip(appName); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show " + appName, + click: () => { + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: "separator" }, + { + label: "Quit", + click: () => { + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (mainWindow.isVisible()) { + mainWindow.focus(); + } else { + mainWindow.show(); + } + }); + + tray.on("double-click", () => { + mainWindow.show(); + mainWindow.focus(); + }); +} + +function getTrayIcon(): string | null { + const candidates = [ + join(__dirname, "..", "assets", "tray-icon.png"), + join(__dirname, "..", "assets", "icon.png"), + join(__dirname, "..", "assets", "icon.ico"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} diff --git a/.benchmark/crm/electron/electron/updater.ts b/.benchmark/crm/electron/electron/updater.ts new file mode 100644 index 0000000..84a01d7 --- /dev/null +++ b/.benchmark/crm/electron/electron/updater.ts @@ -0,0 +1,87 @@ +import { autoUpdater } from "electron-updater"; +import { ipcMain, BrowserWindow } from "electron"; + +let updateAvailable = false; +let updateDownloaded = false; + +export function setupUpdater(appName: string): void { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.logger = { + info: (msg) => console.log("[updater]", msg), + warn: (msg) => console.warn("[updater]", msg), + error: (msg) => console.error("[updater]", msg), + debug: (msg) => console.debug("[updater]", msg), + }; + + autoUpdater.on("checking-for-update", () => { + console.log("[updater] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + updateAvailable = true; + console.log("[updater] Update available:", info.version); + broadcast("updater:available", info); + }); + + autoUpdater.on("update-not-available", (info) => { + console.log("[updater] Up to date:", info.version); + }); + + autoUpdater.on("download-progress", (progress) => { + broadcast("updater:progress", { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + updateDownloaded = true; + console.log("[updater] Update downloaded:", info.version); + broadcast("updater:downloaded", info); + }); + + autoUpdater.on("error", (err) => { + console.error("[updater] Error:", err.message); + broadcast("updater:error", err.message); + }); + + ipcMain.handle("updater:check", async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo ?? null; + } catch (err) { + console.error("[updater] Check failed:", err); + return null; + } + }); + + ipcMain.handle("updater:download", async () => { + if (!updateAvailable) return; + try { + await autoUpdater.downloadUpdate(); + } catch (err) { + console.error("[updater] Download failed:", err); + } + }); + + ipcMain.on("updater:quitAndInstall", () => { + if (updateDownloaded) { + autoUpdater.quitAndInstall(false, true); + } + }); + + setTimeout(() => { + autoUpdater.checkForUpdates().catch(() => {}); + }, 10_000); +} + +function broadcast(channel: string, data: unknown): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, data); + } + } +} diff --git a/.benchmark/crm/electron/electron/utils.ts b/.benchmark/crm/electron/electron/utils.ts new file mode 100644 index 0000000..891a294 --- /dev/null +++ b/.benchmark/crm/electron/electron/utils.ts @@ -0,0 +1,80 @@ +import { app } from "electron"; +import { join } from "path"; +import { existsSync } from "fs"; + +// โ”€โ”€โ”€ Single Instance Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function checkSingleInstance(): boolean { + const gotLock = app.requestSingleInstanceLock(); + if (!gotLock) { + app.quit(); + return false; + } + + app.on("second-instance", (_event, _argv, _cwd) => { + const windows = require("electron").BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const win = windows[0]; + if (win.isMinimized()) win.restore(); + win.focus(); + } + }); + + return true; +} + +// โ”€โ”€โ”€ Dev mode detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function isDev(): boolean { + return !app.isPackaged; +} + +// โ”€โ”€โ”€ Web URL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getWebUrl(dev: boolean, port: number): string { + if (dev) { + return "http://localhost:" + port; + } + + const appPath = app.isPackaged + ? join(process.resourcesPath, "web") + : join(__dirname, "..", "..", "web", "out"); + + const staticIndex = join(appPath, "index.html"); + if (existsSync(staticIndex)) { + return staticIndex; + } + + const standalone = join(appPath, ".next", "standalone", "server.js"); + if (existsSync(standalone)) { + return "http://localhost:3000"; + } + + const fallback = join(appPath, "index.html"); + if (existsSync(fallback)) { + return fallback; + } + + console.warn("[utils] No web build found at", appPath); + return "http://localhost:3000"; +} + +// โ”€โ”€โ”€ API Path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPath(): string | null { + if (app.isPackaged) { + const packed = join(process.resourcesPath, "api", "dist", "index.js"); + if (existsSync(packed)) return packed; + const packedSrc = join(process.resourcesPath, "api", "src", "index.js"); + if (existsSync(packedSrc)) return packedSrc; + } + + const devPath = join(__dirname, "..", "..", "api", "dist", "index.js"); + if (existsSync(devPath)) return devPath; + + const devSrc = join(__dirname, "..", "..", "api", "src", "index.js"); + if (existsSync(devSrc)) return devSrc; + + return null; +} + +// โ”€โ”€โ”€ Get API port from env โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export function getApiPort(): number { + return 3001; +} diff --git a/.benchmark/crm/electron/entitlements.mac.plist b/.benchmark/crm/electron/entitlements.mac.plist new file mode 100644 index 0000000..21c2566 --- /dev/null +++ b/.benchmark/crm/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/.benchmark/crm/electron/package.json b/.benchmark/crm/electron/package.json new file mode 100644 index 0000000..c3e0509 --- /dev/null +++ b/.benchmark/crm/electron/package.json @@ -0,0 +1,41 @@ +{ + "name": "noteapp-desktop", + "version": "1.0.0", + "private": true, + "description": "noteapp โ€” Desktop Application powered by Electron", + "main": "dist/electron/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:web\" \"npm run dev:api\" \"npm run dev:electron\"", + "dev:electron": "tsc && electron .", + "dev:web": "cd ../web && npm run dev", + "dev:api": "cd ../api && npm run dev", + "build": "npm run build:web && npm run build:api && npm run build:electron", + "build:web": "cd ../web && npm run build", + "build:api": "cd ../api && npm run build", + "build:electron": "tsc && electron-builder --config electron-builder.yml", + "build:electron:win": "tsc && electron-builder --win --config electron-builder.yml", + "build:electron:mac": "tsc && electron-builder --mac --config electron-builder.yml", + "build:electron:linux": "tsc && electron-builder --linux --config electron-builder.yml", + "pack": "tsc && electron-builder --dir --config electron-builder.yml", + "typecheck": "tsc --noEmit", + "lint": "eslint electron/" + }, + "dependencies": { + "electron-updater": "^6.3.9", + "electron-store": "^10.0.0" + }, + "devDependencies": { + "electron": "^35.0.0", + "electron-builder": "^25.1.8", + "concurrently": "^9.1.0", + "typescript": "^5.7.0", + "@types/node": "^22.10.0" + }, + "build": { + "productName": "noteapp", + "appId": "com.noteapp.desktop", + "directories": { + "output": "release" + } + } +} \ No newline at end of file diff --git a/.benchmark/crm/electron/tsconfig.json b/.benchmark/crm/electron/tsconfig.json new file mode 100644 index 0000000..6c76752 --- /dev/null +++ b/.benchmark/crm/electron/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": [ + "ES2022" + ], + "outDir": "dist", + "rootDir": "electron", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": [ + "node" + ] + }, + "include": [ + "electron/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "release" + ] +} \ No newline at end of file diff --git a/.benchmark/crm/frontend/app/customers/page.tsx b/.benchmark/crm/frontend/app/customers/page.tsx new file mode 100644 index 0000000..a8cba9e --- /dev/null +++ b/.benchmark/crm/frontend/app/customers/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useCustomers } from "@/hooks/useCustomers"; + +export default function PagePage() { + const { data, loading, error, refetch } = useCustomers(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๅฎขๆˆท็ฎก็†

+

ๅฎขๆˆท็ฎก็†

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/crm/frontend/app/dashboard/page.tsx b/.benchmark/crm/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..9e19a76 --- /dev/null +++ b/.benchmark/crm/frontend/app/dashboard/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useDashboard } from "@/hooks/useDashboard"; + +export default function PagePage() { + const { data, loading, error, refetch } = useDashboard(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜

+

ๆ•ฐๆฎๅˆ†ๆžไปช่กจ็›˜

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/crm/frontend/app/follow_ups/page.tsx b/.benchmark/crm/frontend/app/follow_ups/page.tsx new file mode 100644 index 0000000..0a81bc3 --- /dev/null +++ b/.benchmark/crm/frontend/app/follow_ups/page.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { EmptyState } from "@/components/ui/EmptyState"; +import { useFollow_ups } from "@/hooks/useFollow_ups"; + +export default function PagePage() { + const { data, loading, error, refetch } = useFollow_ups(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+

่ทŸ่ฟ›่ฎฐๅฝ•

+

่ทŸ่ฟ›่ฎฐๅฝ•

+
+ +
+ + {loading ? ( +
+
+
+ ) : error ? ( + +

{error}

+ +
+ ) : data.length === 0 ? ( + setOpen(true)} variant="primary" size="sm">ๅˆ›ๅปบ็ฌฌไธ€ๆก} + /> + ) : ( +
+ {data.map((item: any) => ( + +

{item.title || item.name || `Item ${item.id.slice(0, 8)}`}

+

{item.createdAt || ""}

+
+ ))} +
+ )} +
+ ); +} diff --git a/.benchmark/crm/frontend/app/globals.css b/.benchmark/crm/frontend/app/globals.css new file mode 100644 index 0000000..29b3490 --- /dev/null +++ b/.benchmark/crm/frontend/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/.benchmark/crm/frontend/app/layout.tsx b/.benchmark/crm/frontend/app/layout.tsx new file mode 100644 index 0000000..61e9ee6 --- /dev/null +++ b/.benchmark/crm/frontend/app/layout.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { BottomNav } from "@/components/layout/BottomNav"; + +export const metadata: Metadata = { + title: "NoteApp", + description: "ไธ€ๆฌพๆ”ฏๆŒๅคš็ซฏๅŒๆญฅใ€Markdown ็ผ–่พ‘ๅ’Œๆ ‡็ญพ็ฎก็†็š„็ฌ”่ฎฐๅบ”็”จใ€‚", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ {/* Desktop Sidebar */} + + {/* Main Content */} +
+
+ {children} +
+
+
+ {/* Mobile Bottom Nav */} + + + + ); +} diff --git a/.benchmark/crm/frontend/app/note/[id]/page.tsx b/.benchmark/crm/frontend/app/note/[id]/page.tsx new file mode 100644 index 0000000..0698205 --- /dev/null +++ b/.benchmark/crm/frontend/app/note/[id]/page.tsx @@ -0,0 +1,25 @@ +"use client"; +import { useState } from "react"; +import { Input } from "@/components/ui/Input"; +import { Button } from "@/components/ui/Button"; + +export default function NoteEditorPage() { + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + + return ( +
+
+

็ผ–่พ‘็ฌ”่ฎฐ

+ +
+ setTitle(e.target.value)} placeholder="็ฌ”่ฎฐๆ ‡้ข˜" className="text-lg font-medium border-0 px-0" /> +