Skip to content

Commit 9e9b40a

Browse files
Merge pull request #14 from owsas/feature/object-config
Feature/object config
2 parents cc1840a + e3d74a0 commit 9e9b40a

File tree

11 files changed

+286
-16
lines changed

11 files changed

+286
-16
lines changed

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,54 @@ A working example can be found here: https://github.com/owsas/parse-cloud-class-
3535
* Parse >=2.0
3636
* Parse >=3.0
3737

38+
## New: Configuration with objects
39+
Starting april 2019 (v1.1.0), it's possible to create classes with configuration objects
40+
41+
Example:
42+
```js
43+
const ParseCloudClass = require('parse-server-addon-cloud-class').ParseCloudClass;
44+
45+
// Create a new configuration object to define the class behaviour.
46+
// All attributes are optional
47+
const gamePoint = {
48+
requiredKeys: ['points'], // all objects saved must have the points attribute
49+
defaultValues: { points: 20 }, // by default, all new objects will have 20 points (if it was not set at the time of creation)
50+
minimumValues: { points: 10 }, // minimum 10 points
51+
maximumValues: { points: 1000 }, // maximum 1000 points
52+
immutableKeys: ['points'], // once set, the points can't be changed (only master can do that)
53+
beforeFind: function(req) {
54+
// Do something here
55+
return req.query;
56+
},
57+
processBeforeSave: async function(req) {
58+
// Do something here
59+
return req.object;
60+
},
61+
afterSave: async function(req) {
62+
// Do something here
63+
return req.object;
64+
},
65+
processBeforeDelete: async function(req) {
66+
// Do something here
67+
return req.object;
68+
},
69+
afterDelete: async function(req) {
70+
// Do something here
71+
return req.object;
72+
}
73+
}
74+
75+
// Create an instance
76+
const gamePointClass = ParseCloudClass.fromObject(gamePoint);
77+
78+
// Configure the class in the main.js cloud file
79+
ParseCloudClass.configureClass(Parse, 'GamePoint', gamePointClass);
80+
```
81+
82+
As you see, instead of defining `beforeSave`, we use `processBeforeSave`. This is because ParseCloudClass uses the `beforeSave` function to wrap up some extra logic that we may not want to rewrite each time. In the same fashion, we use `processBeforeDelete`.
83+
84+
With this new functionality, the `this` keyword inside the `beforeFind`, `processBeforeSave`, `afterSave`, `processBeforeDelete` and `beforeDelete` functions refers to the instance itself, which means you can access for example `this.requiredKeys`, etc.
85+
3886
## Basic Usage
3987
```js
4088
/*
@@ -267,6 +315,24 @@ class Addon1 extends ParseCloudClass {
267315
}
268316
```
269317

318+
Now you can also create addons using the new configuration objects, for example:
319+
320+
```js
321+
const dbAddon = {
322+
afterSave: async function(req) {
323+
// replicate data to the other db
324+
return req.object;
325+
},
326+
afterDelete: async function(req) {
327+
// replicate data to the other db
328+
return req.object;
329+
}
330+
}
331+
332+
const addonInstance = ParseCloudClass.fromObject(dbAddon);
333+
```
334+
335+
270336
## Addons
271337

272338
* Algolia Search: https://github.com/owsas/parse-server-addon-cloud-algolia

src/ParseCloudClass.ts

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,41 @@ export interface IProcessResponse {
2929
error: ((e: any) => void);
3030
}
3131

32+
export interface IConstructorParams {
33+
requiredKeys?: string[];
34+
defaultValues?: {[key: string]: any};
35+
minimumValues?: {[key: string]: number};
36+
maximumValues?: {[key: string]: number};
37+
immutableKeys?: string[];
38+
}
39+
40+
/**
41+
* Interface that describes a configuration object
42+
*/
43+
export interface ICloudClassObject extends IConstructorParams {
44+
addons?: ParseCloudClass[];
45+
46+
beforeFind?: (
47+
req: Parse.Cloud.BeforeFindRequest,
48+
) => Parse.Query;
49+
50+
processBeforeSave?: (
51+
req: Parse.Cloud.BeforeSaveRequest,
52+
) => Promise<Parse.Object>;
53+
54+
afterSave?: (
55+
req: Parse.Cloud.AfterSaveRequest,
56+
) => Promise<Parse.Object>;
57+
58+
processBeforeDelete?: (
59+
req: Parse.Cloud.BeforeSaveRequest,
60+
) => Promise<Parse.Object>;
61+
62+
afterDelete?: (
63+
req: Parse.Cloud.AfterSaveRequest,
64+
) => Promise<Parse.Object>;
65+
}
66+
3267
/**
3368
* Defines the methods every ParseCloudClass should have
3469
*/
@@ -79,13 +114,7 @@ export default class ParseCloudClass implements IParseCloudClass {
79114
addons: ParseCloudClass [] = [];
80115
immutableKeys: string[] = [];
81116

82-
constructor (params?: {
83-
requiredKeys?: string[],
84-
defaultValues?: {[key: string]: any},
85-
minimumValues?: {[key: string]: number},
86-
maximumValues?: {[key: string]: number},
87-
immutableKeys?: string[],
88-
}) {
117+
constructor (params?: IConstructorParams) {
89118
if (params) {
90119
if (params.requiredKeys) {
91120
this.requiredKeys = params.requiredKeys;
@@ -116,6 +145,84 @@ export default class ParseCloudClass implements IParseCloudClass {
116145
this.useAddon = this.useAddon.bind(this);
117146
}
118147

148+
/**
149+
* Get a class configuration based
150+
* on a JSON object
151+
* @param object
152+
*/
153+
static fromObject (object: ICloudClassObject): ParseCloudClass {
154+
// Create a class that extends the ParseCloudClass and adds behaviours
155+
// set in the object
156+
class ExtendedCloudClass extends ParseCloudClass {
157+
beforeFind(req: Parse.Cloud.BeforeFindRequest): Parse.Query {
158+
let query = super.beforeFind(req);
159+
160+
if (object.beforeFind) {
161+
query = object.beforeFind.bind(this)(req);
162+
}
163+
164+
return query;
165+
}
166+
167+
async processBeforeSave (
168+
req: Parse.Cloud.BeforeSaveRequest | IProcessRequest,
169+
): Promise<Parse.Object> {
170+
let obj = await super.processBeforeSave(req);
171+
172+
if (object.processBeforeSave) {
173+
obj = await object.processBeforeSave.bind(this)(req);
174+
}
175+
176+
return obj;
177+
}
178+
179+
async afterSave(req: Parse.Cloud.AfterSaveRequest): Promise<Parse.Object> {
180+
let obj = await super.afterSave(req);
181+
182+
if (object.afterSave) {
183+
obj = await object.afterSave.bind(this)(req);
184+
}
185+
186+
return obj;
187+
}
188+
189+
async processBeforeDelete (
190+
req: Parse.Cloud.BeforeDeleteRequest | IProcessRequest,
191+
): Promise<Parse.Object> {
192+
let obj = await super.processBeforeDelete(req);
193+
194+
if (object.processBeforeDelete) {
195+
obj = await object.processBeforeDelete.bind(this)(req);
196+
}
197+
198+
return obj;
199+
}
200+
201+
async afterDelete(
202+
req: Parse.Cloud.AfterDeleteRequest,
203+
): Promise<Parse.Object> {
204+
let obj = await super.afterDelete(req);
205+
206+
if (object.afterDelete) {
207+
obj = await object.afterDelete.bind(this)(req);
208+
}
209+
210+
return obj;
211+
}
212+
}
213+
214+
const cloudClass = new ExtendedCloudClass(object);
215+
216+
// Attach the addons
217+
if (object.addons && object.addons.length) {
218+
object.addons.forEach((addon) => {
219+
cloudClass.useAddon(addon);
220+
});
221+
}
222+
223+
return cloudClass;
224+
}
225+
119226
/**
120227
* Configures a class for working on Parse Cloud
121228
* @param P The Parse Cloud Object
@@ -261,7 +368,7 @@ export default class ParseCloudClass implements IParseCloudClass {
261368
}
262369

263370
req.object = await this.processBeforeSave(req);
264-
if (res) {
371+
if (res && res.success) {
265372
(res as any).success(req.object);
266373
} else {
267374
return req.object;

test/cloud/AnalyticAddon.js renamed to src/test/cloud/AnalyticAddon.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const Parse = require('parse/node');
2-
const { ParseCloudClass } = require('../../src');
2+
const { ParseCloudClass } = require('../../');
33

44
class AnalyticAddon extends ParseCloudClass {
55
async afterSave(req) {

test/cloud/TestAddon.js renamed to src/test/cloud/TestAddon.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { ParseCloudClass } = require('../../src');
1+
const { ParseCloudClass } = require('../../');
22

33
/**
44
* Sets the key 'testAddonProcessed' to true

test/cloud/main.js renamed to src/test/cloud/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { ParseCloudClass } = require('../../src');
1+
const { ParseCloudClass } = require('../../');
22
const TestAddon = require('./TestAddon');
33
const AnalyticAddon = require('./AnalyticAddon');
44

File renamed without changes.

test/index.test.ts renamed to src/test/index.test.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @ts-check
22
import * as Parse from 'parse/node';
3-
import { ParseCloudClass } from '../src/index';
3+
import { ParseCloudClass } from '../index';
4+
import { ICloudClassObject } from '../ParseCloudClass';
45

56
class ExtendedParseClass extends ParseCloudClass {
67
requiredKeys = ['name', 'creditCard', 'description'];
@@ -610,3 +611,99 @@ describe('Working with addons', () => {
610611
});
611612

612613
});
614+
615+
describe('Given a configuration object', () => {
616+
const spyFind = jest.fn();
617+
const spyBeforeSave = jest.fn();
618+
const spyAfterSave = jest.fn();
619+
const spyBeforeDelete = jest.fn();
620+
const spyAfterDelete = jest.fn();
621+
622+
const object: ICloudClassObject = {
623+
addons: [new ParseCloudClass()],
624+
defaultValues: {
625+
test: true,
626+
},
627+
beforeFind: function (req) {
628+
spyFind(this);
629+
return req.query;
630+
},
631+
processBeforeSave: async function (req) {
632+
spyBeforeSave(this);
633+
return req.object;
634+
},
635+
afterSave: async function (req) {
636+
spyAfterSave(this);
637+
return req.object;
638+
},
639+
processBeforeDelete: async function (req) {
640+
spyBeforeDelete(this);
641+
return req.object;
642+
},
643+
afterDelete: async function (req) {
644+
spyAfterDelete(this);
645+
return req.object;
646+
},
647+
};
648+
649+
const instance = ParseCloudClass.fromObject(object);
650+
651+
test('should return a instance of ParseCloudClass', () => {
652+
expect(instance).toBeInstanceOf(ParseCloudClass);
653+
});
654+
655+
describe('Calling beforeFind', () => {
656+
test('should call the mock function', () => {
657+
instance.beforeFind({ query: new Parse.Query('Test') });
658+
expect(spyFind).toHaveBeenCalledTimes(1);
659+
});
660+
661+
test('"this" must reference the instance', () => {
662+
expect(spyFind.mock.calls[0][0].defaultValues).toEqual(object.defaultValues);
663+
});
664+
});
665+
666+
describe('Calling processBeforeSave', () => {
667+
test('should call the mock function', async () => {
668+
await instance.processBeforeSave({ object: new Parse.Object('Test') });
669+
expect(spyBeforeSave).toHaveBeenCalledTimes(1);
670+
});
671+
672+
test('"this" must reference the instance', () => {
673+
expect(spyBeforeSave.mock.calls[0][0].defaultValues).toEqual(object.defaultValues);
674+
});
675+
});
676+
677+
describe('Calling afterSave', () => {
678+
test('should call the mock function', async () => {
679+
await instance.afterSave({ object: new Parse.Object('Test') });
680+
expect(spyAfterSave).toHaveBeenCalledTimes(1);
681+
});
682+
683+
test('"this" must reference the instance', () => {
684+
expect(spyAfterSave.mock.calls[0][0].defaultValues).toEqual(object.defaultValues);
685+
});
686+
});
687+
688+
describe('Calling processBeforeDelete', () => {
689+
test('should call the mock function', async () => {
690+
await instance.processBeforeDelete({ object: new Parse.Object('Test') });
691+
expect(spyBeforeDelete).toHaveBeenCalledTimes(1);
692+
});
693+
694+
test('"this" must reference the instance', () => {
695+
expect(spyBeforeDelete.mock.calls[0][0].defaultValues).toEqual(object.defaultValues);
696+
});
697+
});
698+
699+
describe('Calling afterDelete', () => {
700+
test('should call the mock function', async () => {
701+
await instance.afterDelete({ object: new Parse.Object('Test') });
702+
expect(spyAfterDelete).toHaveBeenCalledTimes(1);
703+
});
704+
705+
test('"this" must reference the instance', () => {
706+
expect(spyAfterDelete.mock.calls[0][0].defaultValues).toEqual(object.defaultValues);
707+
});
708+
});
709+
});

test/util/requireKey.test.ts renamed to src/test/util/requireKey.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ParseCloudClass } from '../../src';
2-
import requireKey from '../../src/decorators/requireKey';
1+
import { ParseCloudClass } from '../../';
2+
import requireKey from '../../decorators/requireKey';
33

44
@requireKey('testKey', 'testKey38')
55
class TestClass extends ParseCloudClass {

test/util/requireLogin.test.ts renamed to src/test/util/requireLogin.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Parse from 'parse/node';
2-
import requireLogin, { PLEASE_LOGIN } from '../../src/util/requireLogin';
2+
import requireLogin, { PLEASE_LOGIN } from '../../util/requireLogin';
33

44
test('Given a request with no user nor master key: should throw', () => {
55
expect(() => {

tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
},
1515
"exclude": [
1616
"node_modules",
17-
"test",
1817
"out"
1918
]
2019
}

0 commit comments

Comments
 (0)