Skip to content

Commit 8c5c6ac

Browse files
committed
V1é
1 parent 8cff5a7 commit 8c5c6ac

17 files changed

Lines changed: 575 additions & 291 deletions

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI - Test
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- v1 # Runs on PRs against the `v1` branch
7+
push:
8+
branches:
9+
- v1 # Runs on pushes to `v1`, e.g., after merging PRs
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
node-version: [14, 18, 23] # Test on Node.js versions 14, 18, and 23
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v2
21+
22+
- name: Set up Node.js
23+
uses: actions/setup-node@v3
24+
with:
25+
node-version: ${{ matrix.node-version }} # Use the Node.js version from the matrix
26+
27+
- name: Install dependencies
28+
run: |
29+
npm ci # Installs dependencies based on the lockfile (package-lock.json)
30+
31+
- name: Build the library
32+
run: npm run build # Runs your build script from package.json
33+
34+
- name: Run tests
35+
run: npm test # Runs your test command (make sure your `package.json` includes a test script)

.github/workflows/node.js.yml

Lines changed: 0 additions & 31 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Release - Test, Build, Publish
2+
3+
on:
4+
release:
5+
types: [created] # Triggers when a new release is created
6+
7+
jobs:
8+
test_build_publish:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
node-version: [14, 18, 23] # Test on Node.js versions 14, 18, and 23
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v2
17+
18+
- name: Set up Node.js
19+
uses: actions/setup-node@v3
20+
with:
21+
node-version: ${{ matrix.node-version }} # Use the Node.js version from the matrix
22+
23+
- name: Install dependencies
24+
run: |
25+
npm ci # Installs dependencies based on the lockfile (package-lock.json)
26+
27+
- name: Build the library
28+
run: npm run build # Runs your build script from package.json
29+
30+
- name: Run tests
31+
run: npm test # Runs your test command
32+
33+
- name: Publish to npm
34+
env:
35+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # Make sure to set NPM_TOKEN as a secret in GitHub
36+
run: npm publish --access public # Publishes the package to npm

lib/core/CouchRepository.d.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,18 @@ declare abstract class CouchRepository {
1111
private connection;
1212
private validator;
1313
private entityClass;
14+
private fieldMap;
1415
constructor(nanoConnection: DocumentScope<Nano.MaybeDocument>, ajvOptions: any, entityClass: EntityClass);
15-
find(id: string): Promise<BaseEntity | null>;
16-
findOrFail(id: string): Promise<BaseEntity>;
17-
findOne(selector: any): Promise<BaseEntity | null>;
18-
findOneOrFail(selector: any): Promise<BaseEntity>;
16+
find(id: string): Promise<BaseEntity>;
17+
findOne(selector: any): Promise<BaseEntity>;
1918
findMany(selector: any): Promise<BaseEntity[]>;
2019
findAll(): Promise<BaseEntity[]>;
2120
create(data: EntityClass): Promise<BaseEntity>;
2221
update(id: string, data: EntityClass): Promise<BaseEntity>;
2322
delete(id: string): Promise<{
2423
message: string;
2524
}>;
26-
static view(connection: DocumentScope<Nano.MaybeDocument>, designname: string, viewname: string, [params]: [any]): Promise<Nano.DocumentViewResponse<unknown, Nano.MaybeDocument>>;
27-
static getFieldNameFromFieldMap(entityClass: EntityClass, entityAttr: string): string;
25+
get dbConnection(): Nano.DocumentScope<Nano.MaybeDocument>;
26+
static getFieldNameFromFieldMap(fieldMap: Record<string, string>, entityAttr: string): string;
2827
}
2928
export default CouchRepository;

lib/core/CouchRepository.js

Lines changed: 44 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1414
Object.defineProperty(exports, "__esModule", { value: true });
1515
const Validation_1 = __importDefault(require("./Validation")); // Import the Validation class
1616
const BaseEntity_1 = __importDefault(require("./BaseEntity"));
17+
const DocumentNotFoundError_1 = require("./DocumentNotFoundError");
18+
const validate = function (object, validator) {
19+
// remove _id and _rev as they are implicitely required
20+
const objectToValidate = JSON.parse(JSON.stringify(object));
21+
delete objectToValidate._id;
22+
delete objectToValidate._rev;
23+
validator.validateData(objectToValidate);
24+
};
1725
// Mango operators
1826
const LOGICAL_OPERATORS = new Set([
1927
'$and', '$or', '$not', '$nor', '$all', '$elemMatch', '$allMatch', '$keyMapMatch'
@@ -62,9 +70,9 @@ function translateSelector(selector, fieldMap) {
6270
}
6371
return translatedSelector;
6472
}
65-
const transformToDocumentFormat = function (data, entityClass) {
73+
const transformToDocumentFormat = function (data, entityClass, fieldMap) {
6674
const transformedData = {};
67-
const fieldMap = entityClass.fieldMap || {};
75+
data.type = entityClass.type; // add type
6876
// Map attributes to the document fields according to fieldMap
6977
for (const [attribute, field] of Object.entries(fieldMap)) {
7078
if (data.hasOwnProperty(attribute)) {
@@ -88,8 +96,6 @@ const transformToDocumentFormat = function (data, entityClass) {
8896
transformedData[attribute] = value;
8997
}
9098
}
91-
const typeField = CouchRepository.getFieldNameFromFieldMap(entityClass, 'type');
92-
transformedData[typeField] = entityClass.type;
9399
return transformedData;
94100
};
95101
// utils
@@ -122,7 +128,7 @@ const inverseTransform = function (document, fieldMap) {
122128
}
123129
return originalData;
124130
};
125-
const findUsingMango = function (query, entityClass, connection) {
131+
const findUsingMango = function (query, entityClass, connection, fieldMap) {
126132
return __awaiter(this, void 0, void 0, function* () {
127133
try {
128134
// Ensure this method is used only from CouchRepository
@@ -133,8 +139,7 @@ const findUsingMango = function (query, entityClass, connection) {
133139
if (!query.selector) {
134140
query.selector = {};
135141
}
136-
query.selector[CouchRepository.getFieldNameFromFieldMap(entityClass, 'type')] = entityClass.type;
137-
console.log(JSON.stringify(query, null, 4));
142+
query.selector[CouchRepository.getFieldNameFromFieldMap(fieldMap, 'type')] = entityClass.type;
138143
// Execute the query
139144
return yield connection.find(query);
140145
}
@@ -147,11 +152,20 @@ class CouchRepository {
147152
constructor(nanoConnection, ajvOptions, entityClass) {
148153
// Check if entityClass extends BaseEntity
149154
if (!(entityClass.prototype instanceof BaseEntity_1.default)) {
150-
throw new Error(`${entityClass.name} must extend BaseEntity`);
155+
throw new Error(`entityClass must extend BaseEntity`);
151156
}
152157
this.connection = nanoConnection;
153158
this.entityClass = entityClass;
154159
this.validator = new Validation_1.default(ajvOptions, this.entityClass.schemaOrSchemaId);
160+
// Get the fieldMap from the entity class, but filter out _id and _rev
161+
const entityFieldMap = entityClass.fieldMap;
162+
// Create a new fieldMap by excluding _id and _rev as keys and values
163+
this.fieldMap = Object.entries(entityFieldMap)
164+
.filter(([key, value]) => key !== '_id' && key !== '_rev' && value !== '_id' && value !== '_rev')
165+
.reduce((acc, [key, value]) => {
166+
acc[key] = value;
167+
return acc;
168+
}, {});
155169
}
156170
// 1. Find a document by its ID
157171
find(id) {
@@ -160,31 +174,8 @@ class CouchRepository {
160174
if (!id) {
161175
throw new Error("ID must be provided");
162176
}
163-
const res = yield findUsingMango({ selector: { _id: id } }, this.entityClass, this.connection);
164-
if (res.docs.length === 0) {
165-
return null; // Document not found
166-
}
167-
return new this.entityClass(inverseTransform(res.docs[0], this.entityClass.fieldMap));
168-
}
169-
catch (err) {
170-
throw err;
171-
}
172-
});
173-
}
174-
// 1. Find a document by its ID
175-
findOrFail(id) {
176-
return __awaiter(this, void 0, void 0, function* () {
177-
try {
178-
if (!id) {
179-
throw new Error("ID must be provided");
180-
}
181-
const res = yield findUsingMango({ selector: { _id: id } }, this.entityClass, this.connection);
182-
if (res.docs.length === 0) {
183-
throw new Error("Document not found");
184-
}
185-
const transformed = inverseTransform(res.docs[0], this.entityClass.fieldMap);
186-
console.log(transformed);
187-
return new this.entityClass(transformed);
177+
const res = this.findOne({ _id: id });
178+
return res;
188179
}
189180
catch (err) {
190181
throw err;
@@ -195,26 +186,11 @@ class CouchRepository {
195186
findOne(selector) {
196187
return __awaiter(this, void 0, void 0, function* () {
197188
try {
198-
const res = yield findUsingMango({ "selector": translateSelector(selector, this.entityClass.fieldMap) }, this.entityClass, this.connection);
189+
const res = yield findUsingMango({ "selector": translateSelector(selector, this.fieldMap) }, this.entityClass, this.connection, this.fieldMap);
199190
if (res.docs.length === 0) {
200-
return null; // No document found
191+
throw new DocumentNotFoundError_1.DocumentNotFoundError(selector);
201192
}
202-
return new this.entityClass(inverseTransform(res.docs[0], this.entityClass.fieldMap));
203-
}
204-
catch (err) {
205-
throw err;
206-
}
207-
});
208-
}
209-
// 3. Find one document or fail
210-
findOneOrFail(selector) {
211-
return __awaiter(this, void 0, void 0, function* () {
212-
try {
213-
const res = yield findUsingMango({ "selector": translateSelector(selector, this.entityClass.fieldMap) }, this.entityClass, this.connection);
214-
if (res.docs.length === 0) {
215-
throw new Error("Document not found");
216-
}
217-
return new this.entityClass(inverseTransform(res.docs[0], this.entityClass.fieldMap));
193+
return new this.entityClass(inverseTransform(res.docs[0], this.fieldMap));
218194
}
219195
catch (err) {
220196
throw err;
@@ -225,8 +201,8 @@ class CouchRepository {
225201
findMany(selector) {
226202
return __awaiter(this, void 0, void 0, function* () {
227203
try {
228-
const res = yield findUsingMango({ "selector": translateSelector(selector, this.entityClass.fieldMap) }, this.entityClass, this.connection);
229-
return res.docs.map((doc) => new this.entityClass(doc));
204+
const res = yield findUsingMango({ "selector": translateSelector(selector, this.fieldMap) }, this.entityClass, this.connection, this.fieldMap);
205+
return res.docs.map((doc) => new this.entityClass(inverseTransform(doc, this.fieldMap)));
230206
}
231207
catch (err) {
232208
throw err;
@@ -237,8 +213,8 @@ class CouchRepository {
237213
findAll() {
238214
return __awaiter(this, void 0, void 0, function* () {
239215
try {
240-
const res = yield findUsingMango({ selector: {} }, this.entityClass, this.connection);
241-
return res.docs.map((doc) => new this.entityClass(doc));
216+
const res = yield findUsingMango({ selector: {} }, this.entityClass, this.connection, this.fieldMap);
217+
return res.docs.map((doc) => new this.entityClass(inverseTransform(doc, this.fieldMap)));
242218
}
243219
catch (err) {
244220
throw err;
@@ -251,12 +227,12 @@ class CouchRepository {
251227
if (!(data instanceof this.entityClass)) {
252228
throw new Error(`Data must be an instance of ${this.entityClass.name}`);
253229
}
254-
const transformedData = transformToDocumentFormat(data, this.entityClass);
255-
console.log(transformedData); // For debugging the transformed data
230+
const transformedData = transformToDocumentFormat(data, this.entityClass, this.fieldMap);
231+
validate(transformedData, this.validator);
256232
// Insert the document into the database
257233
const response = yield this.connection.insert(transformedData);
258234
// Return the newly created entity
259-
return this.findOrFail(response.id);
235+
return this.find(response.id);
260236
}
261237
catch (err) {
262238
throw err;
@@ -271,10 +247,13 @@ class CouchRepository {
271247
if (existingDoc === null) {
272248
throw new Error("Document does not exists");
273249
}
274-
const updatedDoc = Object.assign(Object.assign(Object.assign({}, existingDoc), data), { [CouchRepository.getFieldNameFromFieldMap(this.entityClass, 'type')]: this.entityClass.type, _id: id, _rev: existingDoc.rev });
275-
this.validator.validate(updatedDoc);
250+
const updatedDoc = transformToDocumentFormat(Object.assign(Object.assign({}, existingDoc), data), this.entityClass, this.fieldMap);
251+
updatedDoc._id = existingDoc.id;
252+
updatedDoc._rev = existingDoc.rev;
253+
validate(updatedDoc, this.validator);
276254
const response = yield this.connection.insert(updatedDoc);
277-
return new this.entityClass(Object.assign(Object.assign({}, updatedDoc), { _rev: response.rev }));
255+
// Return the newly created entity
256+
return this.find(response.id);
278257
}
279258
catch (err) {
280259
throw err;
@@ -297,20 +276,11 @@ class CouchRepository {
297276
}
298277
});
299278
}
300-
// 9. Use a view to fetch data
301-
static view(connection_1, designname_1, viewname_1, _a) {
302-
return __awaiter(this, arguments, void 0, function* (connection, designname, viewname, [params]) {
303-
try {
304-
return yield connection.view(designname, viewname, params);
305-
}
306-
catch (err) {
307-
throw err;
308-
}
309-
});
279+
// expose the connection
280+
get dbConnection() {
281+
return this.connection;
310282
}
311-
static getFieldNameFromFieldMap(entityClass, entityAttr) {
312-
// Check if fieldMap exists and contains the given entity attribute
313-
const fieldMap = entityClass.fieldMap || {};
283+
static getFieldNameFromFieldMap(fieldMap, entityAttr) {
314284
// If a custom mapping exists in the fieldMap, return it
315285
if (fieldMap[entityAttr]) {
316286
return fieldMap[entityAttr];
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export declare class DocumentNotFoundError extends Error {
2+
selector: Record<string, any>;
3+
constructor(selector: Record<string, any>);
4+
}

lib/core/DocumentNotFoundError.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.DocumentNotFoundError = void 0;
4+
class DocumentNotFoundError extends Error {
5+
constructor(selector) {
6+
super(`Document not found for the given criteria`);
7+
this.name = 'DocumentNotFindError';
8+
this.selector = selector;
9+
// Ensures the stack trace is correctly preserved in V8 engines
10+
if (Error.captureStackTrace) {
11+
Error.captureStackTrace(this, DocumentNotFoundError);
12+
}
13+
}
14+
}
15+
exports.DocumentNotFoundError = DocumentNotFoundError;

lib/core/Validation.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
44
};
55
Object.defineProperty(exports, "__esModule", { value: true });
66
const ajv_1 = __importDefault(require("ajv")); // Import Ajv types
7+
const ValidationError_1 = require("./ValidationError");
78
class Validation {
89
constructor(ajvOptions, schemaOrSchemaId) {
910
// Initialize the AJV instance with options
@@ -29,12 +30,9 @@ class Validation {
2930
var _a;
3031
const valid = this.validate(data); // Validate the data
3132
if (!valid) {
32-
// Construct an error message with all validation issues
33-
const errorDetails = (_a = this.validate.errors) === null || _a === void 0 ? void 0 : _a.map((err) => {
34-
return JSON.stringify(err).replace(/\"/g, '');
35-
}).join(', ');
36-
// Throw an error with detailed validation messages
37-
throw new Error(`Validation failed: ${errorDetails}`);
33+
const errorDetails = (_a = this.validate.errors) === null || _a === void 0 ? void 0 : _a.map((err) => JSON.stringify(err).replace(/\"/g, '')).join(', ');
34+
// Throwing custom validation error with detailed error message
35+
throw new ValidationError_1.ValidationError(errorDetails || 'Unknown validation error');
3836
}
3937
}
4038
}

0 commit comments

Comments
 (0)