Skip to content

Commit d54b5b9

Browse files
authored
Testing (#39)
* Updated create tables to kysley and added test endpoitns * Fixed kysley bugs * Bug fixes
1 parent 293f36a commit d54b5b9

14 files changed

Lines changed: 648 additions & 66 deletions

File tree

config/demo.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"appEnv": "demo",
3+
"appName": "LessonsApi",
4+
"contentRoot": "http://localhost:3501/content",
5+
"fileStore": "disk",
6+
"mailSystem": "",
7+
"s3Bucket": "",
8+
"transcodePipeline": "",
9+
"transcodePreset": ""
10+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"deploy-staging": "npm-run-all build exec-deploy-staging",
1212
"exec-deploy-prod": "serverless deploy --stage Prod",
1313
"deploy-prod": "npm-run-all build exec-deploy-prod",
14-
"initdb": "ts-node tools/initdb",
14+
"initdb": "tsx tools/initdb.ts",
15+
"populateDemo": "tsx tools/initdb.ts --demo-only",
16+
"reset-demo": "tsx tools/reset-demo.ts",
1517
"migrate": "tsx tools/migrate.ts",
1618
"migrate:up": "tsx tools/migrate.ts --action=up",
1719
"migrate:down": "tsx tools/migrate.ts --action=down",

src/App.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import express from "express";
77
import { CustomAuthProvider } from "@churchapps/apihelper";
88
import cors from "cors";
99

10+
// Kysely's mysql2 driver returns BigInt for ResultSetHeader fields
11+
// (affectedRows, insertId, etc). Without this, controllers that return repo
12+
// delete/update results fail with "Do not know how to serialize a BigInt".
13+
(BigInt.prototype as any).toJSON = function () { return this.toString(); };
14+
1015
export const init = async () => {
1116
dotenv.config();
1217
const container = new Container();
@@ -32,6 +37,13 @@ export const init = async () => {
3237
res.sendStatus(200);
3338
});
3439

40+
// Standard JSON / urlencoded parsing for local dev. In Lambda the body
41+
// arrives as a Buffer via serverless-express and the custom handler below
42+
// takes over (bodyParser is a no-op in that case because the stream is
43+
// already consumed).
44+
expApp.use(express.json({ limit: "50mb" }));
45+
expApp.use(express.urlencoded({ extended: true, limit: "50mb" }));
46+
3547
// Handle body parsing from @codegenie/serverless-express
3648
expApp.use((req, res, next) => {
3749
const contentType = req.headers["content-type"] || "";
Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { controller, httpGet } from "inversify-express-utils";
22
import express from "express";
3+
import { sql } from "kysely";
4+
import { getDb } from "../db";
35

46
@controller("")
57
export class HealthController {
6-
@httpGet("/health")
7-
public async health(req: express.Request, res: express.Response): Promise<void> {
8-
res.status(200).json({ status: "ok", timestamp: new Date().toISOString() });
9-
}
10-
118
@httpGet("/")
129
public async root(req: express.Request, res: express.Response): Promise<void> {
1310
res.status(200).json({ name: "LessonsApi", version: "1.0.0", status: "running", timestamp: new Date().toISOString() });
@@ -18,3 +15,24 @@ export class HealthController {
1815
res.status(204).end();
1916
}
2017
}
18+
19+
@controller("/health")
20+
export class HealthCheckController {
21+
@httpGet("/")
22+
public async health(req: express.Request, res: express.Response): Promise<void> {
23+
const environment = process.env.APP_ENV || "unknown";
24+
const loaded: string[] = [];
25+
try {
26+
await sql`SELECT 1`.execute(getDb());
27+
loaded.push("lessons");
28+
} catch {
29+
// Leave loaded empty so callers know the lessons DB is not reachable.
30+
}
31+
res.status(200).json({
32+
status: loaded.length === 1 ? "healthy" : "degraded",
33+
environment,
34+
modules: { loaded },
35+
timestamp: new Date().toISOString(),
36+
});
37+
}
38+
}

src/controllers/LessonController.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,11 @@ export class LessonController extends LessonsBaseController {
211211
else {
212212
const resources = await this.repositories.resource.loadByContentTypeId(au.churchId, "lesson", id);
213213
if (resources.length > 0) return this.json({}, 401);
214-
else return await this.repositories.lesson.delete(au.churchId, id);
214+
else {
215+
await this.repositories.lesson.delete(au.churchId, id);
216+
await FileStorageHelper.remove("/lessons/" + id + ".png").catch(() => { });
217+
return { id };
218+
}
215219
}
216220
});
217221
}

src/controllers/ProgramController.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,18 @@ export class ProgramController extends LessonsBaseController {
7171
program.churchId = au.churchId;
7272
const p = program;
7373
const saveFunction = async () => {
74-
if (p.image && p.image.startsWith("data:image/")) await this.saveImage(p);
75-
return await this.repositories.program.save(p);
74+
// Save the program first so it has an id, then upload the image
75+
// keyed on that id. (Previously the image was uploaded before the
76+
// id existed, so every upload landed at programs/undefined.png.)
77+
const dataUrlPending = p.image && p.image.startsWith("data:image/") ? p.image : null;
78+
if (dataUrlPending) p.image = "";
79+
const saved = await this.repositories.program.save(p);
80+
if (dataUrlPending) {
81+
saved.image = dataUrlPending;
82+
await this.saveImage(saved);
83+
await this.repositories.program.save(saved);
84+
}
85+
return saved;
7686
};
7787
promises.push(saveFunction());
7888
});
@@ -91,7 +101,12 @@ export class ProgramController extends LessonsBaseController {
91101
const resources = await this.repositories.resource.loadByContentTypeId(au.churchId, "program", id);
92102
const studies = await this.repositories.study.loadByProgramId(au.churchId, id);
93103
if (resources.length > 0 || studies.length > 0) return this.json({}, 401);
94-
else return await this.repositories.program.delete(au.churchId, id);
104+
else {
105+
await this.repositories.program.delete(au.churchId, id);
106+
// Clean up the uploaded image file (if any) so disk doesn't leak.
107+
await FileStorageHelper.remove("/programs/" + id + ".png").catch(() => { });
108+
return { id };
109+
}
95110
}
96111
});
97112
}

src/controllers/StudyController.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ export class StudyController extends LessonsBaseController {
115115
const resources = await this.repositories.resource.loadByContentTypeId(au.churchId, "study", id);
116116
const lessons = await this.repositories.lesson.loadByStudyId(au.churchId, id);
117117
if (resources.length > 0 || lessons.length > 0) return this.json({}, 401);
118-
else return await this.repositories.study.delete(au.churchId, id);
118+
else {
119+
await this.repositories.study.delete(au.churchId, id);
120+
await FileStorageHelper.remove("/studies/" + id + ".png").catch(() => { });
121+
return { id };
122+
}
119123
}
120124
});
121125
}

src/controllers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export { DownloadController } from "./DownloadController";
1010
export { ExternalProviderController } from "./ExternalProviderController";
1111
export { ExternalVideoController } from "./ExternalVideoController";
1212
export { FileController } from "./FileController";
13-
export { HealthController } from "./HealthController";
13+
export { HealthController, HealthCheckController } from "./HealthController";
1414
export { IpDetailController } from "./IpDetailController";
1515
export { LessonController } from "./LessonController";
1616
export { PingbackController } from "./PingbackController";

src/helpers/Environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class Environment extends EnvironmentBase {
1414

1515
static async init(environment: string) {
1616
let file = "dev.json";
17+
if (environment === "demo") file = "demo.json";
1718
if (environment === "staging") file = "staging.json";
1819
if (environment === "prod") file = "prod.json";
1920

src/helpers/FilesHelper.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ export class FilesHelper {
2323
const resource = await Repositories.getCurrent().resource.load(churchId, resourceId);
2424
const bundle = await Repositories.getCurrent().bundle.load(churchId, resource.bundleId);
2525
const oldKey = "files/" + bundle.contentType + "/" + bundle.contentId + "/" + resource.id;
26-
await FileStorageHelper.removeFolder(oldKey);
26+
// FileStorageHelper.removeLocalFolder uses fs.rmdirSync which throws ENOENT
27+
// if the folder doesn't exist yet (e.g. resource has no files). That's not
28+
// an error worth surfacing — swallow it.
29+
try { await FileStorageHelper.removeFolder(oldKey); } catch { /* folder absent */ }
2730
}
2831

2932
static async deleteBundleFolder(churchId: string, bundleId: string) {
3033
const bundle = await Repositories.getCurrent().bundle.load(churchId, bundleId);
3134
const oldKey = "bundles/" + bundle.contentType + "/" + bundle.contentId + "/" + bundle.id;
32-
await FileStorageHelper.removeFolder(oldKey);
35+
try { await FileStorageHelper.removeFolder(oldKey); } catch { /* folder absent */ }
3336
}
3437
}

0 commit comments

Comments
 (0)