From ca0bc6270a5ea85da43cfd406622bd9fe6015896 Mon Sep 17 00:00:00 2001 From: Ewout Stortenbeker Date: Sat, 20 May 2023 13:15:11 +0200 Subject: [PATCH 1/2] add warnOnly option to schema definitions --- src/acebase-base.ts | 4 +- src/api.ts | 2 +- src/schema.spec.ts | 426 ++++++++++++++++++++++++++++++++++++++++++++ src/schema.ts | 14 +- 4 files changed, 440 insertions(+), 6 deletions(-) create mode 100644 src/schema.spec.ts diff --git a/src/acebase-base.ts b/src/acebase-base.ts index 95192c5..0a14398 100644 --- a/src/acebase-base.ts +++ b/src/acebase-base.ts @@ -219,8 +219,8 @@ export abstract class AceBaseBase extends SimpleEventEmitter { get: (path: string) => { return this.api.getSchema(path); }, - set: (path: string, schema: Record|string) => { - return this.api.setSchema(path, schema); + set: (path: string, schema: Record|string, warnOnly = false) => { + return this.api.setSchema(path, schema, warnOnly); }, all: () => { return this.api.getSchemas(); diff --git a/src/api.ts b/src/api.ts index ea2f01b..c407621 100644 --- a/src/api.ts +++ b/src/api.ts @@ -334,7 +334,7 @@ export abstract class Api extends SimpleEventEmitter { deleteIndex(filePath: string): Promise { throw new NotImplementedError('deleteIndex'); } - setSchema(path: string, schema: Record | string): Promise { throw new NotImplementedError('setSchema'); } + setSchema(path: string, schema: Record | string, warnOnly?: boolean): Promise { throw new NotImplementedError('setSchema'); } getSchema(path: string): Promise { throw new NotImplementedError('getSchema'); } diff --git a/src/schema.spec.ts b/src/schema.spec.ts new file mode 100644 index 0000000..e8f10b4 --- /dev/null +++ b/src/schema.spec.ts @@ -0,0 +1,426 @@ +import { SchemaDefinition } from './schema'; + +describe('schema', () => { + const ok = { ok: true }; + + it('can be defined with strings and objects', async () => { + // Try using string type definitions + const clientSchema1 = new SchemaDefinition({ + name: 'string', + url: 'string', + email: 'string', + 'contacts?': { + '*': { + type: 'string', + name: 'string', + email: 'string', + telephone: 'string', + }, + }, + 'addresses?': { + '*': { + type: '"postal"|"visit"', + street: 'string', + nr: 'number', + city: 'string', + 'state?': 'string', + country: '"nl"|"be"|"de"|"fr"', + }, + }, + }); + + // Test if we can add client without contacts and addresses + let result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '' }, false); + expect(result).toEqual({ ok: true }); + + // Test without email + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '' }, false); + expect(result.ok).toBeFalse(); + + // Test with wrong email data type + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: 35 }, false); + expect(result.ok).toBeFalse(); + + // Test with invalid property + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', wrong: 'not allowed' }, false); + expect(result.ok).toBeFalse(); + + // Test with wrong contact + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: 'none' }, false); + expect(result.ok).toBeFalse(); + + // Test with empty contacts + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: { } }, false); + expect(result).toEqual({ ok: true }); + + // Test with wrong contact item data type + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: { contact1: 'wrong contact' } }, false); + expect(result.ok).toBeFalse(); + + // Test with ok contact item + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: { contact1: { type: 'sales', name: 'John', email: '', telephone: '' } } }, false); + expect(result).toEqual({ ok: true }); + + // Test wrong contact item on target path + result = clientSchema1.check('clients/client1', 'wrong contact', false, ['contacts', 'contact1']); + expect(result.ok).toBeFalse(); + + // Test with ok contact item on target path + result = clientSchema1.check('clients/client1', { type: 'sales', name: 'John', email: '', telephone: '' }, false, ['contacts', 'contact1']); + expect(result).toEqual({ ok: true }); + + // Test updating a single property + result = clientSchema1.check('clients/client1', { name: 'John' }, true); + expect(result).toEqual({ ok: true }); + + // Test removing a mandatory property + result = clientSchema1.check('clients/client1', { name: null }, true); + expect(result.ok).toBeFalse(); + + // Test removing an optional property + result = clientSchema1.check('clients/client1', { addresses: null }, true); + expect(result).toEqual({ ok: true }); + + // Test removing an unknown property + result = clientSchema1.check('clients/client1', { unknown: null }, true); + expect(result).toEqual({ ok: true }); + + // Try using classnames & regular expressions + const emailRegex = /[a-z.\-_]+@(?:[a-z\-_]+\.){1,}[a-z]{2,}$/i; + const clientSchema2 = new SchemaDefinition({ + name: String, + url: /^https:\/\//, + email: emailRegex, + 'contacts?': { + '*': { + type: String, + name: String, + email: emailRegex, + telephone: /^\+[0-9\-]{10,}$/, + }, + }, + 'addresses?': { + '*': { + type: '"postal"|"visit"', + street: String, + nr: Number, + city: String, + 'state?': String, + country: /^[A-Z]{2}$/, + }, + }, + }); + + // Test valid input + result = clientSchema2.check('clients/client1', { name: 'My client', url: 'https://client.com', email: 'info@client.com' }, false); + expect(result).toEqual({ ok: true }); + + // Test with empty email + result = clientSchema2.check('clients/client1', '', false, ['email']); + expect(result.ok).toBeFalse(); + + // Test with invalid email + result = clientSchema2.check('clients/client1', 'not valid @address.com', false, ['email']); + expect(result.ok).toBeFalse(); + + // Test with valid email + result = clientSchema2.check('clients/client1', 'test@address.com', false, ['email']); + expect(result).toEqual({ ok: true }); + + // Test valid address + result = clientSchema2.check('clients/client1', { type: 'visit', street: 'Main', nr: 253, city: 'Capital', country: 'NL' }, false, ['addresses', 'address1']); + expect(result).toEqual({ ok: true }); + + // Test invalid address type + result = clientSchema2.check('clients/client1', { type: 'invalid', street: 'Main', nr: 253, city: 'Capital', country: 'NL' }, false, ['addresses', 'address1']); + expect(result.ok).toBeFalse(); + + // Test invalid country (lowercase) + result = clientSchema2.check('clients/client1', { type: 'postal', street: 'Main', nr: 253, city: 'Capital', country: 'nl' }, false, ['addresses', 'address1']); + expect(result.ok).toBeFalse(); + + // Test updating property to valid value + result = clientSchema2.check('clients/client1', { country: 'NL' }, true, ['addresses', 'address1']); + expect(result).toEqual({ ok: true }); + + // Test updating property to invalid value + result = clientSchema2.check('clients/client1', { country: 'nl' }, true, ['addresses', 'address1']); + expect(result.ok).toBeFalse(); + + // Test updating target to valid value + result = clientSchema2.check('clients/client1', 'NL', true, ['addresses', 'address1', 'country']); + expect(result).toEqual({ ok: true }); + + // Test updating target to invalid value + result = clientSchema2.check('clients/client1', 'nl', true, ['addresses', 'address1', 'country']); + expect(result.ok).toBeFalse(); + + // Create new schema to test static values + const staticValuesSchema = new SchemaDefinition({ + 'bool?': true, + 'int?': 35, + 'float?': 101.101, + }); + + // Test valid boolean value: + result = staticValuesSchema.check('static', { bool: true }, false); + expect(result).toEqual({ ok: true }); + + // Test invalid boolean value: + result = staticValuesSchema.check('static', { bool: false }, false); + expect(result.ok).toBeFalse(); + + // Test valid int value: + result = staticValuesSchema.check('static', { int: 35 }, false); + expect(result).toEqual({ ok: true }); + + // Test invalid int value: + result = staticValuesSchema.check('static', { int: 2323 }, false); + expect(result.ok).toBeFalse(); + + // Test valid float value: + result = staticValuesSchema.check('static', { float: 101.101 }, false); + expect(result).toEqual({ ok: true }); + + // Test invalid float value: + result = staticValuesSchema.check('static', { float: 897.452 }, false); + expect(result.ok).toBeFalse(); + }); + + it('with warnOnly enabled', async () => { + const warnOptions = { + warnOnly: true, + warnCallback: (warning: string) => { + console.log(`Expected warning: ${warning}`); + }, + }; + + // Try using string type definitions + const clientSchema1 = new SchemaDefinition({ + name: 'string', + url: 'string', + email: 'string', + 'contacts?': { + '*': { + type: 'string', + name: 'string', + email: 'string', + telephone: 'string', + }, + }, + 'addresses?': { + '*': { + type: '"postal"|"visit"', + street: 'string', + nr: 'number', + city: 'string', + 'state?': 'string', + country: '"nl"|"be"|"de"|"fr"', + }, + }, + }, warnOptions); + + // Test if we can add client without contacts and addresses + let result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '' }, false); + expect(result).toEqual({ ok: true }); + + // Test without email + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '' }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with wrong email data type + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: 35 }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with invalid property + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', wrong: 'not allowed' }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with wrong contact + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: 'none' }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with empty contacts + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: { } }, false); + expect(result).toEqual({ ok: true }); + + // Test with wrong contact item data type + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: { contact1: 'wrong contact' } }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with ok contact item + result = clientSchema1.check('clients/client1', { name: 'Ewout', url: '', email: '', contacts: { contact1: { type: 'sales', name: 'John', email: '', telephone: '' } } }, false); + expect(result).toEqual({ ok: true }); + + // Test wrong contact item on target path + result = clientSchema1.check('clients/client1', 'wrong contact', false, ['contacts', 'contact1']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with ok contact item on target path + result = clientSchema1.check('clients/client1', { type: 'sales', name: 'John', email: '', telephone: '' }, false, ['contacts', 'contact1']); + expect(result).toEqual({ ok: true }); + + // Test updating a single property + result = clientSchema1.check('clients/client1', { name: 'John' }, true); + expect(result).toEqual({ ok: true }); + + // Test removing a mandatory property + result = clientSchema1.check('clients/client1', { name: null }, true); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test removing an optional property + result = clientSchema1.check('clients/client1', { addresses: null }, true); + expect(result).toEqual({ ok: true }); + + // Test removing an unknown property + result = clientSchema1.check('clients/client1', { unknown: null }, true); + expect(result).toEqual({ ok: true }); + + // Try using classnames & regular expressions + const emailRegex = /[a-z.\-_]+@(?:[a-z\-_]+\.){1,}[a-z]{2,}$/i; + const clientSchema2 = new SchemaDefinition({ + name: String, + url: /^https:\/\//, + email: emailRegex, + 'contacts?': { + '*': { + type: String, + name: String, + email: emailRegex, + telephone: /^\+[0-9\-]{10,}$/, + }, + }, + 'addresses?': { + '*': { + type: '"postal"|"visit"', + street: String, + nr: Number, + city: String, + 'state?': String, + country: /^[A-Z]{2}$/, + }, + }, + }, warnOptions); + + // Test valid input + result = clientSchema2.check('clients/client1', { name: 'My client', url: 'https://client.com', email: 'info@client.com' }, false); + expect(result).toEqual({ ok: true }); + + // Test with empty email + result = clientSchema2.check('clients/client1', '', false, ['email']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with invalid email + result = clientSchema2.check('clients/client1', 'not valid @address.com', false, ['email']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test with valid email + result = clientSchema2.check('clients/client1', 'test@address.com', false, ['email']); + expect(result).toEqual({ ok: true }); + + // Test valid address + result = clientSchema2.check('clients/client1', { type: 'visit', street: 'Main', nr: 253, city: 'Capital', country: 'NL' }, false, ['addresses', 'address1']); + expect(result).toEqual({ ok: true }); + + // Test invalid address type + result = clientSchema2.check('clients/client1', { type: 'invalid', street: 'Main', nr: 253, city: 'Capital', country: 'NL' }, false, ['addresses', 'address1']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test invalid country (lowercase) + result = clientSchema2.check('clients/client1', { type: 'postal', street: 'Main', nr: 253, city: 'Capital', country: 'nl' }, false, ['addresses', 'address1']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test updating property to valid value + result = clientSchema2.check('clients/client1', { country: 'NL' }, true, ['addresses', 'address1']); + expect(result).toEqual({ ok: true }); + + // Test updating property to invalid value + result = clientSchema2.check('clients/client1', { country: 'nl' }, true, ['addresses', 'address1']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test updating target to valid value + result = clientSchema2.check('clients/client1', 'NL', true, ['addresses', 'address1', 'country']); + expect(result).toEqual({ ok: true }); + + // Test updating target to invalid value + result = clientSchema2.check('clients/client1', 'nl', true, ['addresses', 'address1', 'country']); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Create new schema to test static values + const staticValuesSchema = new SchemaDefinition({ + 'bool?': true, + 'int?': 35, + 'float?': 101.101, + }, warnOptions); + + // Test valid boolean value: + result = staticValuesSchema.check('static', { bool: true }, false); + expect(result).toEqual({ ok: true }); + + // Test invalid boolean value: + result = staticValuesSchema.check('static', { bool: false }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test valid int value: + result = staticValuesSchema.check('static', { int: 35 }, false); + expect(result).toEqual({ ok: true }); + + // Test invalid int value: + result = staticValuesSchema.check('static', { int: 2323 }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + + // Test valid float value: + result = staticValuesSchema.check('static', { float: 101.101 }, false); + expect(result).toEqual({ ok: true }); + + // Test invalid float value: + result = staticValuesSchema.check('static', { float: 897.452 }, false); + expect(result.ok).toBeTrue(); + expect(result.reason).not.toBeUndefined(); + expect(result.warning).not.toBeUndefined(); + }); + + it('type Object must allow any property', async() => { + const schema = new SchemaDefinition('Object'); + + let result = schema.check('generic-object', { custom: 'allowed' }, false); + expect(result).toEqual(ok); + + result = schema.check('generic-object','allowed', false, ['custom']); + expect(result).toEqual(ok); + + result = schema.check('generic-object', 'NOT allowed', false); + expect(result.ok).toBeFalse(); + }); + +}); diff --git a/src/schema.ts b/src/schema.ts index 45ded8f..b887923 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -249,7 +249,8 @@ function checkObject(path: string, properties: IProperty[], obj: Record) : ISchemaCheckResult { const ok = { ok: true }; @@ -331,7 +332,7 @@ export class SchemaDefinition { readonly source: string|object; readonly text: string; readonly type: IType; - constructor(definition: string|object) { + constructor(definition: string|object, public readonly handling: { warnOnly: boolean, warnCallback?: (message: string) => void } = { warnOnly: false }) { this.source = definition; if (typeof definition === 'object') { // Turn object into typescript definitions @@ -369,6 +370,13 @@ export class SchemaDefinition { this.type = parse(this.text); } check(path: string, value: any, partial: boolean, trailKeys?: Array) : ISchemaCheckResult { - return checkType(path, this.type, value, partial, trailKeys); + const result = checkType(path, this.type, value, partial, trailKeys); + if (!result.ok && this.handling.warnOnly) { + // Only issue a warning, allows schema definitions to be added to a production db to monitor if they are accurate before enforcing them. + result.warning = `${partial ? 'Partial schema' : 'Schema'} check on path "${path}"${trailKeys ? ` for child "${trailKeys.join('/')}"` : ''} failed: ${result.reason}`; + result.ok = true; + this.handling.warnCallback(result.warning); + } + return result; } } From bd9230f0b6c5c00852596d0529751aeceff043fe Mon Sep 17 00:00:00 2001 From: Ewout Stortenbeker Date: Sat, 20 May 2023 13:16:42 +0200 Subject: [PATCH 2/2] Refactored unit tests to TypeScript --- spec/support/jasmine.json | 4 +- spec/transport.spec.js | 238 ---------------- spec/cuid.spec.js => src/cuid.spec.ts | 8 +- .../path-info.spec.ts | 2 +- .../cache.spec.js => src/simple-cache.spec.ts | 11 +- .../simple-event-emitter.spec.ts | 18 +- src/transport.spec.ts | 262 ++++++++++++++++++ spec/utils.spec.js => src/utils.spec.ts | 22 +- 8 files changed, 300 insertions(+), 265 deletions(-) delete mode 100644 spec/transport.spec.js rename spec/cuid.spec.js => src/cuid.spec.ts (77%) rename spec/path-info.spec.js => src/path-info.spec.ts (99%) rename spec/cache.spec.js => src/simple-cache.spec.ts (81%) rename spec/simple-event-emitter.spec.js => src/simple-event-emitter.spec.ts (89%) create mode 100644 src/transport.spec.ts rename spec/utils.spec.js => src/utils.spec.ts (87%) diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index ec99a39..ad93747 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -1,7 +1,7 @@ { - "spec_dir": "spec", + "spec_dir": "", "spec_files": [ - "**/*[sS]pec.js" + "dist/cjs/**/*[sS]pec.js" ], "helpers": [ "helpers/**/*.js" diff --git a/spec/transport.spec.js b/spec/transport.spec.js deleted file mode 100644 index 2a45bf1..0000000 --- a/spec/transport.spec.js +++ /dev/null @@ -1,238 +0,0 @@ -const { serialize, deserialize, serialize2, deserialize2, detectSerializeVersion } = require('../dist/cjs/transport'); -const { PartialArray } = require('../dist/cjs/partial-array'); -const { encodeString } = require('../dist/cjs/utils'); -const { PathReference } = require('../dist/cjs/path-reference'); - -describe('Transport (de)serializing', () => { - - it('single values', () => { - - // v1 date - let val = new Date(); - let ser = serialize(val); - expect(ser).toEqual({ map: 'date', val: val.toISOString() }); - let check = deserialize(ser); - expect(check).toEqual(val); - let ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 date - ser = serialize2(val); - expect(ser).toEqual({ '.type': 'date', '.val': val.toISOString() }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - // v1 regexp - val = /test/ig; - ser = serialize(val); - expect(ser).toEqual({ map: 'regexp', val: { pattern: 'test', flags: 'gi' } }); - check = deserialize(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 regexp - ser = serialize2(val); - expect(ser).toEqual({ '.type': 'regexp', '.val': `/${val.source}/${val.flags}` }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - // v1 binary - val = encodeString('AceBase rocks').buffer; - ser = serialize(val); - expect(ser).toEqual({ map: 'binary', val: `<~6"=Im@<6!&Ec5H'Er~>` }); - check = deserialize(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 binary - ser = serialize2(val); - expect(ser).toEqual({ '.type': 'binary', '.val': `<~6"=Im@<6!&Ec5H'Er~>` }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - // v1 path reference - val = new PathReference('other/path'); - ser = serialize(val); - expect(ser).toEqual({ map: 'reference', val: `other/path` }); - check = deserialize(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 path reference - ser = serialize2(val); - expect(ser).toEqual({ '.type': 'reference', '.val': `other/path` }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - // v1 bigint - let str = '2983834762734857652534876237876233438476'; - val = BigInt(str); - ser = serialize(val); - expect(ser).toEqual({ map: 'bigint', val: str }); - check = deserialize(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 bigint - ser = serialize2(val); - expect(ser).toEqual({ '.type': 'bigint', '.val': str }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - }); - - it('object values', () => { - - // v1 object with date property - let val = { text: 'Some text', date: new Date() }; - let ser = serialize(val); - expect(ser).toEqual({ map: { 'date': 'date' }, val: { text: val.text, date: val.date.toISOString() } }); - let check = deserialize(ser); - expect(check).toEqual(val); - let ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 - ser = serialize2(val); - expect(ser).toEqual({ text: val.text, date: { '.type': 'date', '.val': val.date.toISOString() } }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - // v1 object without serializable property - val = { text: 'Some text' }; - ser = serialize(val); - expect(ser).toEqual({ val: { text: val.text } }); - check = deserialize(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 - ser = serialize2(val); - expect(ser).toEqual(val); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - // v1 object with multiple nested properties that need serializing - val = { - text: 'Some text', - date: new Date('2022-04-22'), - sub1: { - edited: new Date(), - sub2: { - changed: new Date('2022-06-01'), - bigNumber: BigInt('986345948793545534'), - }, - }, - }; - ser = serialize(val); - expect(ser).toEqual({ - map: { - 'date': 'date', - 'sub1/edited': 'date', - 'sub1/sub2/changed': 'date', - 'sub1/sub2/bigNumber': 'bigint', - }, - val: { - text: val.text, - date: val.date.toISOString(), - sub1: { - edited: val.sub1.edited.toISOString(), - sub2: { - changed: val.sub1.sub2.changed.toISOString(), - bigNumber: val.sub1.sub2.bigNumber.toString(), - }, - }, - }, - }); - check = deserialize(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 - ser = serialize2(val); - expect(ser).toEqual({ - text: val.text, - date: { '.type': 'date', '.val': val.date.toISOString() }, - sub1: { - edited: { '.type': 'date', '.val': val.sub1.edited.toISOString() }, - sub2: { - changed: { '.type': 'date', '.val': val.sub1.sub2.changed.toISOString() }, - bigNumber: { '.type': 'bigint', '.val': val.sub1.sub2.bigNumber.toString() }, - }, - }, - }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - - }); - - it('partial (sparse) arrays', () => { - // v1 partial array: - let val = new PartialArray({ - 5: 'text', - 12: new Date(), - 26: { date: new Date() }, - }); - let ser = serialize(val); - expect(ser).toEqual({ - map: { - '': 'array', - '12': 'date', - '26/date': 'date', - }, - val: new PartialArray({ - 5: val[5], - 12: val[12].toISOString(), - 26: { - date: val[26].date.toISOString(), - }, - }), - }); - let check = deserialize(ser); - expect(check).toEqual(val); - let ver = detectSerializeVersion(ser); - expect(ver).toBe(1); - - // v2 date - ser = serialize2(val); - expect(ser).toEqual({ - '.type': 'array', - 5: val[5], - 12: { - '.type': 'date', - '.val': val[12].toISOString(), - }, - 26: { - date: { - '.type': 'date', - '.val': val[26].date.toISOString(), - }, - }, - }); - check = deserialize2(ser); - expect(check).toEqual(val); - ver = detectSerializeVersion(ser); - expect(ver).toBe(2); - }); -}); diff --git a/spec/cuid.spec.js b/src/cuid.spec.ts similarity index 77% rename from spec/cuid.spec.js rename to src/cuid.spec.ts index afc8837..9cdf3aa 100644 --- a/spec/cuid.spec.js +++ b/src/cuid.spec.ts @@ -1,5 +1,5 @@ -const cuid = require('../dist/cjs/cuid').default; -const hiresCuid = require('../dist/cjs/cuid/hires').default; +import cuid from './cuid'; +import hiresCuid from './cuid/hires'; describe('cuid', function() { it ('high resolution', () => { @@ -11,7 +11,7 @@ describe('cuid', function() { // Generate hires cuids const n = 100000; - let cuids = new Array(n); + const cuids = new Array(n) as string[]; for (let i = 0; i < n; i++) { cuids[i] = hiresCuid(); } @@ -19,7 +19,7 @@ describe('cuid', function() { // Expect all first 11 chars (7 ms + 4 ns) to be different and lexicographically sortable (cuid[n] < cuid[n+1]) for (let i = 1; i < n; i++) { const t1 = cuids[i-1].slice(1, 12), t2 = cuids[i].slice(1, 12); - expect(t1).toBeLessThan(t2); + expect(t1 < t2).toBeTrue(); } // debugger; diff --git a/spec/path-info.spec.js b/src/path-info.spec.ts similarity index 99% rename from spec/path-info.spec.js rename to src/path-info.spec.ts index 3a00a1f..dd2de78 100644 --- a/spec/path-info.spec.js +++ b/src/path-info.spec.ts @@ -1,4 +1,4 @@ -const { PathInfo } = require('../dist/cjs/path-info'); +import { PathInfo } from './path-info'; describe('PathInfo', function() { diff --git a/spec/cache.spec.js b/src/simple-cache.spec.ts similarity index 81% rename from spec/cache.spec.js rename to src/simple-cache.spec.ts index fceea26..b06fbbf 100644 --- a/spec/cache.spec.js +++ b/src/simple-cache.spec.ts @@ -1,10 +1,9 @@ -/** @type {import("../types/simple").SimpleCache} */ -const { SimpleCache } = require('../dist/cjs/simple-cache'); +import { SimpleCache } from './simple-cache'; describe('cache', function() { it('maxEntries without expirySeconds', async () => { - const wait = async ms => new Promise(resolve => setTimeout(resolve, ms)); + const wait = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const cache = new SimpleCache({ maxEntries: 10 }); cache.set(1, '1'); cache.set(2, '2'); @@ -25,12 +24,12 @@ describe('cache', function() { cache.set(11, '11'); expect(cache.size).toBe(10); expect(cache.get(1)).toBeNull(); - const accessed1 = cache.cache.get(2).accessed; + const accessed1 = (cache as any).cache.get(2).accessed; await wait(2); // Make sure the clock ticks > 1ms.. expect(cache.get(2)).toBe('2'); - const accessed2 = cache.cache.get(2).accessed; + const accessed2 = (cache as any).cache.get(2).accessed; expect(accessed1).toBeLessThan(accessed2); cache.set(12, '12'); @@ -42,7 +41,7 @@ describe('cache', function() { it('maxEntries with expirySeconds', async () => { const cache = new SimpleCache({ maxEntries: 5, expirySeconds: 10 }); - const wait = async ms => new Promise(resolve => setTimeout(resolve, ms)); + const wait = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); cache.set(1, '1'); cache.set(2, '2'); diff --git a/spec/simple-event-emitter.spec.js b/src/simple-event-emitter.spec.ts similarity index 89% rename from spec/simple-event-emitter.spec.js rename to src/simple-event-emitter.spec.ts index d849882..9fbf5cc 100644 --- a/spec/simple-event-emitter.spec.js +++ b/src/simple-event-emitter.spec.ts @@ -1,4 +1,4 @@ -const { SimpleEventEmitter } = require('../dist/cjs/simple-event-emitter'); +import { SimpleEventEmitter } from './simple-event-emitter'; describe('Simple Event Emitter', () => { it('once', async () => { @@ -6,10 +6,10 @@ describe('Simple Event Emitter', () => { const emitter = new SimpleEventEmitter(); const result = { - promise1: null, - callback1: null, - promise2: null, - callback2: null, + promise1: null as any, + callback1: null as any, + promise2: null as any, + callback2: null as any, }; // Test callback @@ -47,10 +47,10 @@ describe('Simple Event Emitter', () => { emitter.emitOnce('test', 'success'); const result = { - promise1: null, - callback1: null, - promise2: null, - callback2: null, + promise1: null as any, + callback1: null as any, + promise2: null as any, + callback2: null as any, }; // Test callback diff --git a/src/transport.spec.ts b/src/transport.spec.ts new file mode 100644 index 0000000..b7377f3 --- /dev/null +++ b/src/transport.spec.ts @@ -0,0 +1,262 @@ +import { serialize, deserialize, serialize2, deserialize2, detectSerializeVersion } from './transport'; +import { PartialArray } from './partial-array'; +import { encodeString } from './utils'; +import { PathReference } from './path-reference'; + +describe('Transport (de)serializing', () => { + + it('single values', () => { + { + // v1 date + const val = new Date(); + const ser = serialize(val); + expect(ser).toEqual({ map: 'date', val: val.toISOString() }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 date + const val = new Date(); + const ser = serialize2(val); + expect(ser as any).toEqual({ '.type': 'date', '.val': val.toISOString() }); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + { + // v1 regexp + const val = /test/ig; + const ser = serialize(val); + expect(ser).toEqual({ map: 'regexp', val: { pattern: 'test', flags: 'gi' } }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 regexp + const val = /test/ig; + const ser = serialize2(val); + expect(ser as any).toEqual({ '.type': 'regexp', '.val': `/${val.source}/${val.flags}` }); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + { + // v1 binary + const val = encodeString('AceBase rocks').buffer; + const ser = serialize(val); + expect(ser).toEqual({ map: 'binary', val: `<~6"=Im@<6!&Ec5H'Er~>` }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 binary + const val = encodeString('AceBase rocks').buffer; + const ser = serialize2(val); + expect(ser as any).toEqual({ '.type': 'binary', '.val': `<~6"=Im@<6!&Ec5H'Er~>` }); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + { + // v1 path reference + const val = new PathReference('other/path'); + const ser = serialize(val); + expect(ser).toEqual({ map: 'reference', val: `other/path` }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 path reference + const val = new PathReference('other/path'); + const ser = serialize2(val); + expect(ser as any).toEqual({ '.type': 'reference', '.val': `other/path` }); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + { + // v1 bigint + const str = '2983834762734857652534876237876233438476'; + const val = BigInt(str); + const ser = serialize(val); + expect(ser).toEqual({ map: 'bigint', val: str }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 bigint + const str = '2983834762734857652534876237876233438476'; + const val = BigInt(str); + const ser = serialize2(val); + expect(ser as any).toEqual({ '.type': 'bigint', '.val': str }); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + }); + + it('object values', () => { + + { + // v1 object with date property + const val = { text: 'Some text', date: new Date() }; + const ser = serialize(val); + expect(ser).toEqual({ map: { 'date': 'date' }, val: { text: val.text, date: val.date.toISOString() } }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 + const val = { text: 'Some text', date: new Date() }; + const ser = serialize2(val); + expect(ser as any).toEqual({ text: val.text, date: { '.type': 'date', '.val': val.date.toISOString() } }); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + { + // v1 object without serializable property + const val = { text: 'Some text' }; + const ser = serialize(val); + expect(ser).toEqual({ val: { text: val.text } }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + } + { + // v2 + const val = { text: 'Some text' }; + const ser = serialize2(val); + expect(ser as any).toEqual(val); + const check = deserialize2(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(2); + } + + { + // v1 object with multiple nested properties that need serializing + const val = { + text: 'Some text', + date: new Date('2022-04-22'), + sub1: { + edited: new Date(), + sub2: { + changed: new Date('2022-06-01'), + bigNumber: BigInt('986345948793545534'), + }, + }, + }; + const ser = serialize(val); + expect(ser).toEqual({ + map: { + 'date': 'date', + 'sub1/edited': 'date', + 'sub1/sub2/changed': 'date', + 'sub1/sub2/bigNumber': 'bigint', + }, + val: { + text: val.text, + date: val.date.toISOString(), + sub1: { + edited: val.sub1.edited.toISOString(), + sub2: { + changed: val.sub1.sub2.changed.toISOString(), + bigNumber: val.sub1.sub2.bigNumber.toString(), + }, + }, + }, + }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + + // v2 + const ser2 = serialize2(val); + expect(ser2 as any).toEqual({ + text: val.text, + date: { '.type': 'date', '.val': val.date.toISOString() }, + sub1: { + edited: { '.type': 'date', '.val': val.sub1.edited.toISOString() }, + sub2: { + changed: { '.type': 'date', '.val': val.sub1.sub2.changed.toISOString() }, + bigNumber: { '.type': 'bigint', '.val': val.sub1.sub2.bigNumber.toString() }, + }, + }, + }); + const check2 = deserialize2(ser2); + expect(check2).toEqual(val); + const ver2 = detectSerializeVersion(ser2); + expect(ver2).toBe(2); + } + }); + + it('partial (sparse) arrays', () => { + // v1 partial array: + const val = new PartialArray({ + 5: 'text', + 12: new Date(), + 26: { date: new Date() }, + }); + const ser = serialize(val); + expect(ser).toEqual({ + map: { + '': 'array', + '12': 'date', + '26/date': 'date', + }, + val: new PartialArray({ + 5: val[5], + 12: val[12].toISOString(), + 26: { + date: val[26].date.toISOString(), + }, + }), + }); + const check = deserialize(ser); + expect(check).toEqual(val); + const ver = detectSerializeVersion(ser); + expect(ver).toBe(1); + + // v2 date + const ser2 = serialize2(val); + expect(ser2 as any).toEqual({ + '.type': 'array', + 5: val[5], + 12: { + '.type': 'date', + '.val': val[12].toISOString(), + }, + 26: { + date: { + '.type': 'date', + '.val': val[26].date.toISOString(), + }, + }, + }); + const check2 = deserialize2(ser2); + expect(check2).toEqual(val); + const ver2 = detectSerializeVersion(ser2); + expect(ver2).toBe(2); + }); +}); diff --git a/spec/utils.spec.js b/src/utils.spec.ts similarity index 87% rename from spec/utils.spec.js rename to src/utils.spec.ts index f748f62..cd64cd9 100644 --- a/spec/utils.spec.js +++ b/src/utils.spec.ts @@ -1,4 +1,5 @@ -const { cloneObject, compareValues, valuesAreEqual, getMutations, ObjectDifferences, bigintToBytes, bytesToBigint } = require('../dist/cjs/utils'); +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { cloneObject, compareValues, valuesAreEqual, getMutations, ObjectDifferences, bigintToBytes, bytesToBigint } from './utils'; describe('Utils', function() { @@ -12,13 +13,12 @@ describe('Utils', function() { participants: ['ewout','pete','john','jack','kenny'], messages: { msg1: { from: 'ewout', sent: Date.now(), text: 'Hey guys what are you all doing tonight?' }, - }, + } as Record, updated: Date.now(), }; it('clone & compare', () => { - /** @type {typeof chat} */ - let chatClone = cloneObject(chat); + let chatClone: typeof chat = cloneObject(chat); // Assert the clone is not the same object reference expect(chatClone !== chat).toBeTrue(); @@ -79,6 +79,7 @@ describe('Utils', function() { it('bigintToBytes & bytesToBigint', () => { // Try 0 + // @ts-ignore no BigInt literals < ES2020 let nr = 0n; let bytes = bigintToBytes(nr); expect(bytes).toEqual([0]); @@ -86,6 +87,7 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try -1 + // @ts-ignore no BigInt literals < ES2020 nr = -1n; bytes = bigintToBytes(nr); expect(bytes).toEqual([255]); @@ -93,6 +95,7 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try max positive number that can be stored using 8 bits (127) + // @ts-ignore no BigInt literals < ES2020 nr = 127n; bytes = bigintToBytes(nr); expect(bytes).toEqual([127]); @@ -100,12 +103,14 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try overflowing the max positive number that can be stored using 8 bits (127) + // @ts-ignore no BigInt literals < ES2020 nr = 128n; bytes = bigintToBytes(nr); expect(bytes).toEqual([0, 128]); // overflow byte needed to prevent marking as negative reverse = bytesToBigint(bytes); expect(reverse).toBe(nr); + // @ts-ignore no BigInt literals < ES2020 nr = 129n; bytes = bigintToBytes(nr); expect(bytes).toEqual([0, 129]); // overflow byte needed to prevent marking as negative @@ -113,6 +118,7 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try max negative number that can be stored using 8 bits (-128) + // @ts-ignore no BigInt literals < ES2020 nr = -128n; bytes = bigintToBytes(nr); expect(bytes).toEqual([128]); @@ -120,12 +126,14 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try overflowing the max negative number that can be stored using 8 bits (-128) + // @ts-ignore no BigInt literals < ES2020 nr = -129n; bytes = bigintToBytes(nr); expect(bytes).toEqual([255, 127]); // overflow byte needed to prevent marking as positive reverse = bytesToBigint(bytes); expect(reverse).toBe(nr); + // @ts-ignore no BigInt literals < ES2020 nr = -130n; bytes = bigintToBytes(nr); expect(bytes).toEqual([255, 126]); // overflow byte needed to prevent marking as positive @@ -133,6 +141,7 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try max positive number that can be stored using 64 bits + // @ts-ignore no BigInt literals < ES2020 nr = (2n ** 63n) - 1n; bytes = bigintToBytes(nr); expect(bytes).toEqual([127,255,255,255,255,255,255,255]); @@ -140,6 +149,7 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try max negative number that can be stored using 64 bits + // @ts-ignore no BigInt literals < ES2020 nr = -(2n ** 63n); bytes = bigintToBytes(nr); expect(bytes).toEqual([128,0,0,0,0,0,0,0]); @@ -147,6 +157,7 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Try a 128 bit number + // @ts-ignore no BigInt literals < ES2020 nr = (2n ** 127n) - (2n ** 64n); bytes = bigintToBytes(nr); expect(bytes).toEqual([ @@ -157,9 +168,10 @@ describe('Utils', function() { expect(reverse).toBe(nr); // Check from -1M to +1M + // @ts-ignore no BigInt literals < ES2020 for (let nr = -1_000_000n; nr < 1_000_000n; nr++) { bytes = bigintToBytes(nr); - let check = bytesToBigint(bytes); + const check = bytesToBigint(bytes); expect(check).toBe(nr); }