@nymphjs/server
Version:
Nymph.js - REST Server
829 lines (676 loc) • 20.5 kB
text/typescript
import express from 'express';
import SQLite3Driver from '@nymphjs/driver-sqlite3';
import { Nymph as NymphServer } from '@nymphjs/nymph';
import { Nymph, Entity, HttpError } from '@nymphjs/client';
import createServer from './index.js';
import {
EmployeeModel as EmployeeModelClass,
Employee as EmployeeClass,
RestrictedModel as RestrictedModelClass,
Restricted as RestrictedClass,
EmployeeData,
} from './testArtifacts.js';
const sqliteConfig = {
filename: ':memory:',
};
const nymphServer = new NymphServer({}, new SQLite3Driver(sqliteConfig));
const EmployeeModel = nymphServer.addEntityClass(EmployeeModelClass);
const RestrictedModel = nymphServer.addEntityClass(RestrictedModelClass);
const app = express();
app.use('/test', createServer(nymphServer));
const server = app.listen(5080);
const nymph = new Nymph({
restUrl: 'http://localhost:5080/test/',
});
const Employee = nymph.addEntityClass(EmployeeClass);
const Restricted = nymph.addEntityClass(RestrictedClass);
describe('Nymph REST Server and Client', () => {
async function createJane() {
const jane = await Employee.factory();
jane.name = 'Jane Doe';
jane.current = true;
jane.salary = 8000000;
jane.startDate = Date.now();
jane.subordinates = [];
jane.title = 'Seniorer Person';
try {
await jane.$save();
} catch (e: any) {
console.error('Error creating entity: ', e);
throw e;
}
return jane;
}
it('create an entity', async () => {
const jane = await createJane();
expect(jane.guid).not.toBeNull();
expect(typeof jane.guid).toEqual('string');
});
it('mdate is updated on save', async () => {
const jane = await createJane();
expect(jane.guid).not.toBeNull();
const oldMdate = jane.mdate ?? Infinity;
await new Promise((resolve) => setTimeout(() => resolve(true), 100));
await jane.$save();
expect(jane.mdate).toBeGreaterThan(oldMdate);
});
it('create two unrelated entities', async () => {
const entity = await Employee.factory();
entity.name = 'Jane Doe';
entity.current = true;
entity.salary = 8000000;
entity.startDate = Date.now();
entity.subordinates = [];
entity.title = 'Seniorer Person';
const entity2 = await Employee.factory();
entity2.name = 'John Doe';
entity2.current = true;
entity2.salary = 8000000;
entity2.startDate = Date.now();
entity2.subordinates = [];
entity2.title = 'Seniorer Person';
const entities = [entity, entity2];
await nymph.saveEntities(entities);
if (entity.guid == null || entity2.guid == null) {
throw new Error('Entity is null.');
}
entity.building = 'J2';
entity2.building = 'B4';
await nymph.saveEntities(entities);
const jane = await nymph.getEntity({ class: Employee }, entity.guid);
const john = await nymph.getEntity({ class: Employee }, entity2.guid);
expect(jane?.building).toEqual('J2');
expect(john?.building).toEqual('B4');
});
it('create two related entities', async () => {
const jane = await Employee.factory();
jane.name = 'Jane Doe';
jane.current = true;
jane.salary = 8000000;
jane.startDate = Date.now();
jane.subordinates = [];
jane.title = 'Seniorer Person';
await jane.$save();
const steve = await Employee.factory();
steve.$addTag('boss', 'bigcheese');
steve.name = 'Steve Guy';
steve.current = true;
steve.salary = 8000000;
steve.startDate = Date.now();
steve.subordinates = [jane];
steve.title = 'Executive Person';
await steve.$save();
if (jane.guid == null || steve.guid == null) {
throw new Error('Entity is null.');
}
const checkSteve = await nymph.getEntity(
{ class: Employee },
{
type: '&',
ref: ['subordinates', jane],
},
);
expect(checkSteve?.guid).toEqual(steve.guid);
const checkSteveQref = await nymph.getEntity(
{ class: Employee },
{
type: '&',
qref: [
'subordinates',
[{ class: Employee }, { type: '&', guid: jane.guid }],
],
},
);
expect(checkSteveQref?.guid).toEqual(steve.guid);
});
it('add, check, and remove tags', async () => {
const entity = await Entity.factory();
entity.$addTag('test');
expect(entity.$hasTag('test')).toEqual(true);
entity.$addTag('test', 'test2');
expect(entity.$hasTag('test', 'test2')).toEqual(true);
entity.$addTag('test', 'test3', 'test4', 'test5', 'test6');
expect(entity.$hasTag('test', 'test3', 'test4', 'test5', 'test6')).toEqual(
true,
);
entity.$removeTag('test2');
expect(!entity.$hasTag('test2')).toEqual(true);
entity.$removeTag('test3', 'test4');
expect(!entity.$hasTag('test3', 'test4')).toEqual(true);
entity.$removeTag('test5', 'test6');
expect(!entity.$hasTag('test5', 'test6')).toEqual(true);
expect(!(entity.tags < ['test'] || entity.tags > ['test'])).toEqual(true);
});
it('wake a sleeping reference', async () => {
const jane = await createJane();
if (jane.guid == null) {
throw new Error('Entity is null.');
}
const entity = Employee.factoryReference([
'nymph_entity_reference',
jane.guid,
'Employee',
]) as EmployeeClass & EmployeeData;
await entity.$wake();
expect(entity?.name).toEqual('Jane Doe');
});
it('change an entity', async () => {
const employee = await createJane();
if (employee.guid == null) {
throw new Error('Entity is null.');
}
delete employee.salary;
employee.current = false;
employee.endDate = Date.now() + 1;
await employee.$save();
const check = await Employee.factory(employee.guid);
expect(check.salary).toBeUndefined();
expect(check.current).toEqual(false);
expect(check.endDate).toBeGreaterThan(check.startDate ?? 0);
});
it('patch an entity', async () => {
const employee = await createJane();
if (employee.guid == null) {
throw new Error('Entity is null.');
}
delete employee.salary;
employee.current = false;
employee.endDate = Date.now() + 1;
await employee.$patch();
const check = await Employee.factory(employee.guid);
expect(check.salary).toBeUndefined();
expect(check.current).toEqual(false);
expect(check.endDate).toBeGreaterThan(check.startDate ?? 0);
});
it('get an entity', async () => {
await createJane();
const jane = await nymph.getEntity(
{
class: Employee,
},
{
type: '&',
equal: ['name', 'Jane Doe'],
},
);
expect(jane).not.toBeNull();
expect(jane?.guid).not.toBeNull();
});
it('get entities', async () => {
for (let i = 0; i < 4; i++) {
await createJane();
}
const entities = await nymph.getEntities(
{
class: Employee,
limit: 4,
},
{
type: '&',
tag: ['employee'],
},
);
expect(entities.length).toEqual(4);
entities.forEach((entity) => {
expect(entity.guid).not.toBeNull();
});
});
it('get entity counts', async () => {
for (let i = 0; i < 20; i++) {
await createJane();
}
const result = await nymph.getEntities({ class: Employee });
// Testing count return...
const resultCount = await nymph.getEntities({
class: Employee,
return: 'count',
});
expect(resultCount).toBeGreaterThanOrEqual(1);
expect(resultCount).toEqual(result.length);
const resultSelectors = await nymph.getEntities(
{ class: Employee },
{ type: '&', equal: ['name', 'Jane Doe'] },
);
// Testing count return with selectors...
const resultSelectorsCount = await nymph.getEntities(
{ class: Employee, return: 'count' },
{ type: '&', equal: ['name', 'Jane Doe'] },
);
expect(resultSelectorsCount).toBeGreaterThanOrEqual(1);
expect(resultSelectorsCount).toEqual(resultSelectors.length);
// Testing count return with limit...
const resultSelectorsLimit = await nymph.getEntities({
class: Employee,
limit: 1,
return: 'count',
});
expect(resultSelectorsLimit).toEqual(1);
// Testing count return with limit...
const resultSelectorsSingle = await nymph.getEntity({
class: Employee,
return: 'count',
});
expect(resultSelectorsSingle).toEqual(1);
// Testing empty count...
const resultSelectorsEmpty = await nymph.getEntities(
{ class: Employee, return: 'count' },
{ type: '&', tag: 'pickle' },
);
expect(resultSelectorsEmpty).toEqual(0);
});
it('get entity GUIDs', async () => {
for (let i = 0; i < 4; i++) {
await createJane();
}
const entities = await nymph.getEntities(
{
class: Employee,
limit: 4,
return: 'guid',
},
{
type: '&',
tag: 'employee',
},
);
expect(entities.length).toEqual(4);
entities.forEach((guid) => {
expect(typeof guid).toEqual('string');
});
});
it('use two selectors', async () => {
for (let i = 0; i < 4; i++) {
await createJane();
}
const entities = await nymph.getEntities(
{
class: Employee,
limit: 4,
},
{
type: '&',
tag: ['employee'],
},
{
type: '&',
like: ['name', '%Jane%'],
},
);
expect(entities.length).toEqual(4);
entities.forEach((entity) => {
expect(entity.guid).not.toBeNull();
});
});
it('use deep selector', async () => {
for (let i = 0; i < 4; i++) {
await createJane();
}
const entities = await nymph.getEntities(
{
class: Employee,
limit: 4,
},
{
type: '&',
tag: ['employee'],
selector: {
type: '|',
like: ['name', '%Jane%'],
},
},
);
expect(entities.length).toEqual(4);
entities.forEach((entity) => {
expect(entity.guid).not.toBeNull();
});
});
it('use really deep selector', async () => {
for (let i = 0; i < 4; i++) {
await createJane();
}
const entities = await nymph.getEntities(
{
class: Employee,
limit: 4,
},
{
type: '&',
tag: ['employee'],
selector: {
type: '|',
selector: [
{
type: '&',
like: ['name', '%Jane%'],
},
{
type: '&',
equal: ['name', 'Penelope'],
},
],
},
},
);
expect(entities.length).toEqual(4);
entities.forEach((entity) => {
expect(entity.guid).not.toBeNull();
});
});
it('delete an entity', async () => {
const jane = await createJane();
const guid = jane.guid;
if (guid == null) {
throw new Error('Entity is null.');
}
const deleted = await jane.$delete();
expect(deleted).toEqual(true);
const check = await nymph.getEntities(
{ class: Employee },
{
type: '&',
guid,
},
);
expect(check.length).toEqual(0);
});
it('delete entities', async () => {
const janes = [await createJane(), await createJane()];
const guids = janes.map((jane) => {
if (jane.guid == null) {
throw new Error('Entity is null.');
}
return jane.guid;
});
const deleted = await nymph.deleteEntities(janes);
expect(deleted).toEqual(guids);
const check = await nymph.getEntities(
{ class: Employee },
{
type: '|',
guid: guids,
},
);
expect(check.length).toEqual(0);
});
it('try to save Entity class directly', async () => {
const entity = new Entity() as Entity & { something: string };
entity.$nymph = nymph;
entity.something = 'Anything';
let error = { message: '' };
try {
await entity.$save();
} catch (e: any) {
error = e;
}
expect(error.message).toEqual(
"Can't use Entity class directly from the front end.",
);
});
it('handle forbidden method', async () => {
let error = { status: 200 };
try {
await Employee.inaccessibleMethod();
} catch (e: any) {
error = e;
}
expect(error.status).toEqual(403);
});
it('handle server side static error', async () => {
let error = { status: 0, error: { name: '' } };
try {
await Employee.throwErrorStatic();
} catch (e: any) {
error = e;
}
expect(error.status).toEqual(500);
expect(error.error.name).toEqual('BadFunctionCallError');
});
it('handle server side static iterator error', async () => {
let error: any = { status: 0, error: { name: '' } };
const data = await Employee.throwErrorStaticIterable();
let count = 0;
for await (let value of data) {
count++;
if (value instanceof Error) {
error = value;
} else {
expect(value).toEqual(count);
}
}
expect(count).toEqual(2);
expect(error.status).toEqual(500);
expect(error.error.name).toEqual('BadFunctionCallError');
});
it('handle server side error', async () => {
const jane = await createJane();
let error = { status: 0, error: { name: '' } };
try {
await jane.$throwError();
} catch (e: any) {
error = e;
}
expect(error.status).toEqual(500);
expect(error.error.name).toEqual('BadFunctionCallError');
});
it('handle server side HTTP error', async () => {
const jane = await createJane();
let error = { status: 0, statusText: '', message: '' };
try {
await jane.$throwHttpError();
} catch (e: any) {
error = e;
}
expect(error.status).toEqual(501);
expect(error.statusText).toEqual('Not Implemented');
expect(error.message).toEqual('A 501 HTTP error.');
});
it('handle server side custom HTTP error', async () => {
const jane = await createJane();
let error = { status: 0, statusText: '', message: '' };
try {
await jane.$throwHttpErrorWithDescription();
} catch (e: any) {
error = e;
}
expect(error.status).toEqual(512);
expect(error.statusText).toEqual('Some Error');
expect(error.message).toEqual('A 512 HTTP error.');
});
it('call a server side static method', async () => {
const data = await Employee.testStatic(5);
expect(data).toEqual(10);
});
it('call a server side static iterator method', async () => {
const data = await Employee.testStaticIterable(5);
let count = 0;
for await (let value of data) {
count++;
expect(value).toEqual(5 + count);
}
expect(count).toEqual(3);
});
it('aborts a server side static iterator method', async () => {
const data = await Employee.testStaticIterableAbort();
data.abortController.abort();
// Wait 1 second to ensure server receives abort signal.
await new Promise((resolve) => setTimeout(resolve, 1000));
expect(true).toBeTruthy();
});
it('call a stateless server side method', async () => {
const jane = await createJane();
const data = await jane?.$testMethodStateless(5);
expect(data).toEqual(6);
expect(jane?.name).toEqual('Jane Doe');
});
it('call a server side method', async () => {
const jane = await createJane();
expect(jane?.current).toEqual(true);
const data = await jane?.$testMethod(5);
expect(data).toEqual(7);
expect(jane?.current).toEqual(false);
});
it('refresh an entity', async () => {
const jane1 = await createJane();
if (jane1.guid == null) {
throw new Error('Entity is null.');
}
const jane2 = await nymph.getEntity(
{
class: Employee,
},
jane1.guid,
);
if (jane1 == null || jane2 == null) {
throw new Error('Entity is null.');
}
jane1.name = 'Janet Doe';
await jane1.$save();
expect(jane2.name).toEqual('Jane Doe');
await jane2.$refresh();
expect(jane2.name).toEqual('Janet Doe');
});
it('check two entities are equal', async () => {
const first = await createJane();
if (first.guid == null) {
throw new Error('Entity is null.');
}
const second = await nymph.getEntity(
{
class: Employee,
},
first.guid,
);
if (second == null || second.guid == null) {
throw new Error('Entity is null.');
}
first.thing = 'this';
first.other = 'this';
second.other = 'this';
second.thing = 'this';
expect(first.$equals(second)).toEqual(true);
second.other = 'that';
second.thing = 'that';
expect(first.$equals(second)).toEqual(false);
});
it('check two objects are the same entity', async () => {
const first = await createJane();
if (first.guid == null) {
throw new Error('Entity is null.');
}
const second = await nymph.getEntity(
{
class: Employee,
},
first.guid,
);
if (second == null || second.guid == null) {
throw new Error('Entity is null.');
}
expect(first.$is(second)).toEqual(true);
second.other = 'this';
second.thing = 'this';
expect(first.$is(second)).toEqual(true);
await createJane();
const third = await nymph.getEntity(
{
class: Employee,
},
{
type: '&',
'!guid': first.guid,
},
);
if (third == null || third.guid == null) {
throw new Error('Entity is null.');
}
expect(first.$is(third)).toEqual(false);
});
it("doesn't allow creation of a restricted entity class", async () => {
const attempt = await Restricted.factory();
attempt.name = 'Jane Doe';
let error = null;
try {
await attempt.$save();
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(HttpError);
expect(error.status).toEqual(403);
});
it("doesn't allow search of a restricted entity class", async () => {
let error = null;
try {
await nymph.getEntity({
class: Restricted,
});
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(HttpError);
expect(error.status).toEqual(403);
});
it("doesn't allow methods of a restricted entity class", async () => {
const attempt = await Restricted.factory();
attempt.name = 'Jane Doe';
let error = null;
try {
await attempt.$testMethod(1);
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(HttpError);
expect(error.status).toEqual(403);
});
it("doesn't allow static methods of a restricted entity class", async () => {
let error = null;
try {
await Restricted.testStatic(1);
} catch (e: any) {
error = e;
}
expect(error).toBeInstanceOf(HttpError);
expect(error.status).toEqual(403);
});
it('get a new UID', async () => {
const uidValue = await nymph.newUID('employee');
expect(typeof uidValue).toEqual('number');
expect(uidValue).toBeGreaterThan(0);
const uidValue2 = await nymph.newUID('employee');
expect(typeof uidValue2).toEqual('number');
expect(uidValue2).toBeGreaterThan(uidValue);
});
it('get UID value', async () => {
const uidValue = await nymph.getUID('employee');
expect(typeof uidValue).toEqual('number');
expect(uidValue).toBeGreaterThan(0);
const uidValue2 = await nymph.getUID('employee');
expect(typeof uidValue2).toEqual('number');
expect(uidValue2).toEqual(uidValue);
});
it('set UID value', async () => {
const uidValue = await nymph.newUID('employee');
expect(typeof uidValue).toEqual('number');
expect(uidValue).toBeGreaterThan(0);
const success = await nymph.setUID('employee', uidValue - 1);
expect(success).toEqual(true);
const uidValue2 = await nymph.getUID('employee');
expect(typeof uidValue2).toEqual('number');
expect(uidValue2).toEqual(uidValue - 1);
});
it('delete UID', async () => {
const uidValue = await nymph.newUID('temp');
expect(typeof uidValue).toEqual('number');
expect(uidValue).toEqual(1);
await nymph.deleteUID('temp');
let error = { status: 200 };
try {
await nymph.getUID('temp');
} catch (e: any) {
error = e;
}
expect(error.status).toEqual(404);
});
afterAll(() => {
server.close(); // avoid jest open handle error
});
});