Skip to content

Commit

Permalink
Merge pull request #854 from 06kellyjac/data_layer
Browse files Browse the repository at this point in the history
refactor(experimental): abstract data layer
  • Loading branch information
JamieSlome authored Jan 9, 2025
2 parents 1df2381 + 4c2a36d commit 21f9bbb
Show file tree
Hide file tree
Showing 20 changed files with 470 additions and 208 deletions.
8 changes: 8 additions & 0 deletions experimental/license-inventory/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions experimental/license-inventory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@types/express": "^5.0.0",
"@types/mongoose": "^5.11.97",
"@types/node": "^22.10.1",
"@types/semver": "^7.5.8",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
Expand Down
51 changes: 27 additions & 24 deletions experimental/license-inventory/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
import express from 'express';
import { logger } from '@/logger';
import apiRouter from '@/routes/api';
import createApiRouter from '@/routes/api';
import pinoHTTP from 'pino-http';
import bodyParser from 'body-parser';
import { rateLimit } from 'express-rate-limit';
import helmet from 'helmet';
import { LicenseDataService } from './services/data';
// import lusca from 'lusca';

// helmet and lusca comparison
// https://github.com/krakenjs/lusca/issues/42#issuecomment-65093906
// TODO: integrate lusca once added sessions/auth

const app = express();
const createApp = (lds: LicenseDataService) => {
const app = express();

const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: 'draft-7',
legacyHeaders: false,
// in memory store
});
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: 'draft-7',
legacyHeaders: false,
// in memory store
});

app.use(helmet());
app.use(limiter);
app.use(bodyParser.json());
app.use(
pinoHTTP({
logger,
autoLogging: process.env.NODE_ENV === 'development',
// overrides core logger redaction
// please update in logger.ts
// redact: [],
}),
);
app.use(helmet());
app.use(limiter);
app.use(bodyParser.json());
app.use(
pinoHTTP({
logger,
autoLogging: process.env.NODE_ENV === 'development',
// overrides core logger redaction
// please update in logger.ts
// redact: [],
}),
);

app.use('/api', apiRouter);

export { app };
app.use('/api', createApiRouter(lds));
return app;
};
export { createApp };
8 changes: 4 additions & 4 deletions experimental/license-inventory/src/db/connect.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AsyncResult } from '@/types';
import mongoose from 'mongoose';
import mongoose, { type Mongoose } from 'mongoose';

export const connectDB = async (dbURI: string): AsyncResult<void> => {
export const connectDB = async (dbURI: string): AsyncResult<Mongoose> => {
try {
await mongoose.connect(dbURI);
return { error: null, data: null };
const connection = await mongoose.connect(dbURI);
return { error: null, data: connection };
} catch (e: unknown) {
if (e instanceof Error) {
return { error: e, data: null };
Expand Down
11 changes: 11 additions & 0 deletions experimental/license-inventory/src/db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Mongoose, Model } from 'mongoose';
import { licenseSchema, type LicenseSchema } from './schemas/license/license';

export class Database {
mongoose: Mongoose;
License: Model<LicenseSchema>;
constructor(mongoose: Mongoose) {
this.mongoose = mongoose;
this.License = mongoose.model<LicenseSchema>('License', licenseSchema);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { License } from '@/db/collections';
import { licenseValidation } from './license';
import { v4 as uuidv4 } from 'uuid';

// these tests require a mongodb instance
Expand All @@ -16,7 +15,7 @@ describeDB('license', () => {

const insertedLicense = await License.findOne({ _id: _id });
expect(insertedLicense).not.toBeNull();
expect(insertedLicense.name).toBe('hello');
expect(insertedLicense!.name).toBe('hello');
});

it('can insert a complex license', async () => {
Expand All @@ -37,30 +36,7 @@ describeDB('license', () => {

const insertedLicense = await License.findOne({ _id: _id });
expect(insertedLicense).not.toBeNull();
expect(insertedLicense.name).toBe('complex');
expect(insertedLicense.chooseALicenseInfo.permissions.commercialUse).toBe(true);
});

it('complex license conforms to output validation', async () => {
const _id = uuidv4();
await License.create({
_id,
name: 'complex2',
spdxID: 'sample2',
chooseALicenseInfo: {
permissions: {
commercialUse: true,
},
conditions: {
networkUseDisclose: false,
},
},
});

const insertedLicense = await (await License.findOne({ _id }).exec()).toJSON();
const { error, data } = licenseValidation.safeParse(insertedLicense);
expect(error).toBeUndefined();
expect(data.id).toBe(_id);
expect(!Object.keys(data).includes('_id'));
expect(insertedLicense!.name).toBe('complex');
expect(insertedLicense!.chooseALicenseInfo?.permissions?.commercialUse).toBe(true);
});
});
21 changes: 0 additions & 21 deletions experimental/license-inventory/src/db/schemas/license/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,6 @@ export const licenseSchema = new Schema<LicenseSchema>(
{
// automatic createdAt updatedAt
timestamps: true,
toObject: {
virtuals: true,
versionKey: false,
transform: (doc, ret) => {
delete ret._id;
},
},
toJSON: {
virtuals: true,
versionKey: false,
transform: (doc, ret) => {
delete ret._id;
},
},
virtuals: {
id: {
get() {
return this._id.toString();
},
},
},
},
);

Expand Down
12 changes: 7 additions & 5 deletions experimental/license-inventory/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { z } from 'zod';

const envSchema = z.object({
PORT: z.coerce.number().default(3000),
MONGO_URI: z.string(),
});
const envSchema = z
.object({
PORT: z.coerce.number().default(3000),
MONGO_URI: z.string(),
})
.required();

const { error, data: env } = envSchema.safeParse(process.env);
if (error) {
console.error(error);
throw new Error('failed to validate');
}

export default env;
export default env as z.infer<typeof envSchema>;
13 changes: 9 additions & 4 deletions experimental/license-inventory/src/routes/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import express from 'express';
import v0Router from './v0';
const router = express.Router();
import createV0Router from './v0';
import { LicenseDataService } from '@/services/data';

router.use('/v0', v0Router);
const createRouter = (lds: LicenseDataService) => {
const router = express.Router();

export default router;
router.use('/v0', createV0Router(lds));
return router;
};

export default createRouter;
13 changes: 9 additions & 4 deletions experimental/license-inventory/src/routes/api/v0/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import express from 'express';
import licensesRouter from './licenses';
const router = express.Router();
import createLicensesRouter from './licenses';
import { LicenseDataService } from '@/services/data';

router.use('/licenses', licensesRouter);
const createRouter = (lds: LicenseDataService) => {
const router = express.Router();

export default router;
router.use('/licenses', createLicensesRouter(lds));
return router;
};

export default createRouter;
65 changes: 52 additions & 13 deletions experimental/license-inventory/src/routes/api/v0/licenses.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect, afterEach, jest } from '@jest/globals';
import request from 'supertest';
import { License } from '@/db/collections';
import { app } from '@/app';
import { v4 as uuidv4 } from 'uuid';
import { createApp } from '@/app';
import { genMockLicenseDataService } from '@/test/mock/db';

const basePath = '/api/v0/licenses';
const genRoute = (p: string) => basePath + p;
Expand All @@ -13,29 +14,67 @@ describe(basePath, () => {

describe('GET / - list', () => {
it('no data', async () => {
const execMock = jest.fn(() => Promise.resolve([]));
jest.spyOn(License, 'find').mockReturnValueOnce({
exec: execMock,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
const mockLDS = genMockLicenseDataService();
mockLDS.list.mockResolvedValueOnce({ error: null, data: [] });
const app = createApp(mockLDS);
const res = await request(app).get(genRoute('/')).expect('Content-Type', /json/).expect(200);

expect(res.body).toEqual([]);
});

it('one entry', async () => {
const inputData = {
id: 'test',
id: uuidv4(),
name: 'test',
};
const execMock = jest.fn(() => Promise.resolve([{ toJSON: async () => inputData }]));
jest.spyOn(License, 'find').mockReturnValueOnce({
exec: execMock,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
const mockLDS = genMockLicenseDataService();
mockLDS.list.mockResolvedValueOnce({ error: null, data: [inputData] });
const app = createApp(mockLDS);
const res = await request(app).get(genRoute('/')).expect('Content-Type', /json/).expect(200);

expect(res.body).toEqual([inputData]);
});
});

describe(`GET /:id - read`, () => {
const testID = '157c0c6a-5c99-4298-9529-95816da2255a';
it('invalid id - not uuid', async () => {
const mockLDS = genMockLicenseDataService();
mockLDS.getByUUID.mockRejectedValueOnce(null);
const app = createApp(mockLDS);
await request(app)
.get(genRoute('/' + 'apache-2_0'))
.expect('Content-Type', /json/)
.expect(500);
expect(mockLDS.getByUUID.mock.lastCall).toBeUndefined();
});

it('valid id - no data', async () => {
const mockLDS = genMockLicenseDataService();
mockLDS.getByUUID.mockResolvedValueOnce({ error: null, data: null });
const app = createApp(mockLDS);
const res = await request(app)
.get(genRoute('/' + testID))
.expect('Content-Type', /json/)
.expect(200);

expect(res.body).toEqual({ license: null });
});

it('valid id - data', async () => {
const licenseData = {
id: testID,
name: 'test',
};
const mockLDS = genMockLicenseDataService();
mockLDS.getByUUID.mockResolvedValueOnce({ error: null, data: licenseData });
const app = createApp(mockLDS);
const res = await request(app)
.get(genRoute('/' + testID))
.expect('Content-Type', /json/)
.expect(200);

expect(res.body).toEqual({ license: licenseData });
});
});
});
Loading

0 comments on commit 21f9bbb

Please sign in to comment.