Commit e0c4d3ff authored by nanahira's avatar nanahira

use own class transform thing

parent 1496a79f
...@@ -15,13 +15,11 @@ ...@@ -15,13 +15,11 @@
"encoded-buffer": "^0.2.6", "encoded-buffer": "^0.2.6",
"generic-pool": "^3.9.0", "generic-pool": "^3.9.0",
"ioredis": "^5.2.3", "ioredis": "^5.2.3",
"lodash": "^4.17.21",
"lru-cache": "^7.13.1", "lru-cache": "^7.13.1",
"typed-reflector": "^1.0.11" "typed-reflector": "^1.0.12"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^28.1.8", "@types/jest": "^28.1.8",
"@types/lodash": "^4.14.182",
"@types/node": "^18.7.23", "@types/node": "^18.7.23",
"@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1", "@typescript-eslint/parser": "^5.38.1",
...@@ -1257,12 +1255,6 @@ ...@@ -1257,12 +1255,6 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "dev": true
}, },
"node_modules/@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.7.23", "version": "18.7.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
...@@ -4724,9 +4716,10 @@ ...@@ -4724,9 +4716,10 @@
} }
}, },
"node_modules/typed-reflector": { "node_modules/typed-reflector": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/typed-reflector/-/typed-reflector-1.0.11.tgz", "resolved": "https://registry.npmjs.org/typed-reflector/-/typed-reflector-1.0.12.tgz",
"integrity": "sha512-OhryVYaR+tBEW9Yt2PsPqAniNfbVk1idKbnLxBCBPUSHVRm+Ajik/QxifoJUuGoaXAZDLW9JlJTO6ctXGZX9gQ==", "integrity": "sha512-GMlwkgutVGKvnWXglo/SuMMJsxnw++ThIzJhFM7O01FD0pBYV+QiFQChlHkh+6vL4AtfDj3GdFaDPqssnn+wCQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
} }
...@@ -5889,12 +5882,6 @@ ...@@ -5889,12 +5882,6 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true "dev": true
}, },
"@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "18.7.23", "version": "18.7.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz",
...@@ -8407,9 +8394,9 @@ ...@@ -8407,9 +8394,9 @@
"dev": true "dev": true
}, },
"typed-reflector": { "typed-reflector": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/typed-reflector/-/typed-reflector-1.0.11.tgz", "resolved": "https://registry.npmjs.org/typed-reflector/-/typed-reflector-1.0.12.tgz",
"integrity": "sha512-OhryVYaR+tBEW9Yt2PsPqAniNfbVk1idKbnLxBCBPUSHVRm+Ajik/QxifoJUuGoaXAZDLW9JlJTO6ctXGZX9gQ==", "integrity": "sha512-GMlwkgutVGKvnWXglo/SuMMJsxnw++ThIzJhFM7O01FD0pBYV+QiFQChlHkh+6vL4AtfDj3GdFaDPqssnn+wCQ==",
"requires": { "requires": {
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
} }
......
...@@ -3,12 +3,11 @@ import { AnyClass, AragamiOptions, Awaitable, ClassType } from './def'; ...@@ -3,12 +3,11 @@ import { AnyClass, AragamiOptions, Awaitable, ClassType } from './def';
import { RedisDriver } from './drivers/redis'; import { RedisDriver } from './drivers/redis';
import { MemoryDriver } from './drivers/memory'; import { MemoryDriver } from './drivers/memory';
import { reflector } from './metadata'; import { reflector } from './metadata';
import { instanceToPlain, plainToInstance } from 'class-transformer';
import { encode, decode } from 'encoded-buffer'; import { encode, decode } from 'encoded-buffer';
import { makeArray, MayBeArray } from './utility/utility'; import { makeArray, MayBeArray } from './utility/utility';
import _ from 'lodash';
import { PartialDeep } from './utility/partial-deep'; import { PartialDeep } from './utility/partial-deep';
import { wrapClass } from './utility/encode-decode';
export class Aragami { export class Aragami {
readonly driver: BaseDriver; readonly driver: BaseDriver;
...@@ -40,7 +39,7 @@ export class Aragami { ...@@ -40,7 +39,7 @@ export class Aragami {
return o; return o;
} }
if (prototype) { if (prototype) {
o = plainToInstance(prototype, o); o = wrapClass(prototype, o);
} }
const keyTransformer = reflector.get('AragamiCacheKey', o); const keyTransformer = reflector.get('AragamiCacheKey', o);
if (!keyTransformer) { if (!keyTransformer) {
...@@ -57,11 +56,11 @@ export class Aragami { ...@@ -57,11 +56,11 @@ export class Aragami {
} }
private encode(o: any) { private encode(o: any) {
return encode(instanceToPlain(o)); return encode(o);
} }
private decode<T>(cl: ClassType<T>, value: Buffer) { private decode<T>(cl: ClassType<T>, value: Buffer) {
return plainToInstance(cl, decode(value)[0]); return wrapClass(cl, decode(value)[0]);
} }
async get<T>(cl: ClassType<T>, key: string) { async get<T>(cl: ClassType<T>, key: string) {
...@@ -99,7 +98,7 @@ export class Aragami { ...@@ -99,7 +98,7 @@ export class Aragami {
return o; return o;
} }
if (prototype) { if (prototype) {
o = plainToInstance(prototype, o); o = wrapClass(prototype, o);
} }
const buf = this.encode(o); const buf = this.encode(o);
await this.driver.set( await this.driver.set(
...@@ -185,11 +184,11 @@ export class Aragami { ...@@ -185,11 +184,11 @@ export class Aragami {
const baseKey = this.getBaseKey(o); const baseKey = this.getBaseKey(o);
const keyTransformers = reflector.getArray('AragamiLockKeys', o); const keyTransformers = reflector.getArray('AragamiLockKeys', o);
const actualKeys = await Promise.all(keyTransformers.map((fn) => fn(o))); const actualKeys = await Promise.all(keyTransformers.map((fn) => fn(o)));
return _.compact( return actualKeys
actualKeys.flatMap((mayBeKeyArray) => .flatMap((mayBeKeyArray) =>
makeArray(mayBeKeyArray).map((key) => `${baseKey}:${key}`), makeArray(mayBeKeyArray).map((key) => `${baseKey}:${key}`),
), )
); .filter((s) => !!s);
} }
async lock<R>( async lock<R>(
...@@ -278,7 +277,7 @@ export class Aragami { ...@@ -278,7 +277,7 @@ export class Aragami {
return o; return o;
} }
if (prototype) { if (prototype) {
o = plainToInstance(prototype, o); o = wrapClass(prototype, o);
} }
const buf = this.encode(o); const buf = this.encode(o);
const key = const key =
......
import { Metadata } from './metadata'; import { Metadata } from './metadata';
import { Awaitable, TypedMethodDecorator } from './def'; import { Awaitable, ClassType, TypedMethodDecorator } from './def';
import { MayBeArray } from './utility/utility'; import { MayBeArray } from './utility/utility';
import { Type } from 'class-transformer';
import { MergePropertyDecorators } from './utility/merge';
export const CacheTTL = (ttl: number): ClassDecorator & MethodDecorator => export const CacheTTL = (ttl: number): ClassDecorator & MethodDecorator =>
Metadata.set('AragamiCacheTTL', ttl); Metadata.set('AragamiCacheTTL', ttl);
...@@ -42,3 +44,6 @@ export const LockKey = DrainKey<MayBeArray<string>>( ...@@ -42,3 +44,6 @@ export const LockKey = DrainKey<MayBeArray<string>>(
(cb) => Metadata.append('AragamiLockKeys', cb), (cb) => Metadata.append('AragamiLockKeys', cb),
(o, key) => `${key}_${o}`, (o, key) => `${key}_${o}`,
); );
export const CastType = <T>(cb: () => ClassType<T>): PropertyDecorator =>
MergePropertyDecorators([Metadata.set('AragamiType', cb), Type(cb)]);
import { MetadataSetter, Reflector } from 'typed-reflector'; import { MetadataSetter, Reflector } from 'typed-reflector';
import { MayBeArray } from './utility/utility'; import { MayBeArray } from './utility/utility';
import { Awaitable } from './def'; import { AnyClass, Awaitable } from './def';
interface MetadataMap { interface MetadataMap {
AragamiCacheTTL: number; AragamiCacheTTL: number;
AragamiCachePrefix: string; AragamiCachePrefix: string;
AragamiCacheKey: (obj: any) => Awaitable<string>; AragamiCacheKey: (obj: any) => Awaitable<string>;
AragamiType: () => AnyClass;
} }
interface MetadataArrayMap { interface MetadataArrayMap {
......
import { AnyClass, ClassType } from '../def';
import { reflector } from '../metadata';
export const dewrapClass = (value: any) => {
return value;
};
function isClass(target: any) {
if (typeof target !== 'function') {
return false;
}
return (
target.constructor === Function &&
(target === Date || target.toString().startsWith('class '))
);
}
export const wrapClass = <T>(
cl: ClassType<T>,
value: any,
visited = new Set(),
): T => {
if (
!isClass(cl) ||
value instanceof cl ||
value == null ||
visited.has(value)
) {
return value;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (cl === Date) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return new Date(value) as any;
}
visited.add(value);
const instance = new cl();
const runField = (key: string, val: any) => {
const meta = reflector.get('AragamiType', cl, key);
let fieldCl: AnyClass;
if (meta) {
fieldCl = meta();
} else {
// use design:type
fieldCl = Reflect.getMetadata('design:type', cl.prototype, key);
}
return wrapClass(fieldCl, val, visited);
};
for (const key of Object.keys(value)) {
const field = value[key];
if (Array.isArray(field)) {
instance[key] = field.map((v: any) => runField(key, v));
} else {
instance[key] = runField(key, field);
}
}
return instance;
};
export function MergePropertyDecorators(
decs: PropertyDecorator[],
): PropertyDecorator {
return (obj, key) => {
for (const dec of decs) {
dec(obj, key);
}
};
}
export function MergeMethodDecorators(
decs: MethodDecorator[],
): MethodDecorator {
return (obj, key, descriptor) => {
for (const dec of decs) {
dec(obj, key, descriptor);
}
};
}
export function MergeClassDecorators(decs: ClassDecorator[]): ClassDecorator {
return (obj) => {
for (const dec of decs) {
dec(obj);
}
};
}
export function MergeParameterDecorators(
decs: ParameterDecorator[],
): ParameterDecorator {
return (obj, key, index) => {
for (const dec of decs) {
dec(obj, key, index);
}
};
}
export function MergeClassOrMethodDecorators(
decs: (ClassDecorator & MethodDecorator)[],
): ClassDecorator & MethodDecorator {
return (obj, key?, descriptor?) => {
if (descriptor) {
for (const dec of decs) {
dec(obj, key, descriptor);
}
} else {
for (const dec of decs) {
dec(obj);
}
}
};
}
...@@ -2,7 +2,6 @@ import { Metadata, reflector } from './metadata'; ...@@ -2,7 +2,6 @@ import { Metadata, reflector } from './metadata';
import { makeArray, MayBeArray } from './utility/utility'; import { makeArray, MayBeArray } from './utility/utility';
import { Awaitable, ClassType, TypedMethodDecorator } from './def'; import { Awaitable, ClassType, TypedMethodDecorator } from './def';
import { Aragami } from './aragami'; import { Aragami } from './aragami';
import _ from 'lodash';
export const WithKey = ( export const WithKey = (
factory: (param: any, obj: any, key: string) => Awaitable<any> = (param) => factory: (param: any, obj: any, key: string) => Awaitable<any> = (param) =>
...@@ -37,8 +36,8 @@ export class WrapDecoratorBuilder { ...@@ -37,8 +36,8 @@ export class WrapDecoratorBuilder {
); );
const keys = ( const keys = (
await Promise.all( await Promise.all(
_.compact( lockKeyParams
lockKeyParams.map(async (fun, i) => { .map(async (fun, i) => {
if (!fun) return; if (!fun) return;
const keyResult = (await fun( const keyResult = (await fun(
args[i], args[i],
...@@ -46,8 +45,8 @@ export class WrapDecoratorBuilder { ...@@ -46,8 +45,8 @@ export class WrapDecoratorBuilder {
key as string, key as string,
)) as MayBeArray<any>; )) as MayBeArray<any>;
return makeArray(keyResult); return makeArray(keyResult);
}), })
), .filter((s) => !!s),
) )
).flat(); ).flat();
return aragami.lock(keys, () => oldFun.apply(this, args)); return aragami.lock(keys, () => oldFun.apply(this, args));
......
...@@ -26,6 +26,10 @@ describe('Aragami.', () => { ...@@ -26,6 +26,10 @@ describe('Aragami.', () => {
async getName() { async getName() {
return `n.${this.name}`; return `n.${this.name}`;
} }
getAge() {
return this.age;
}
} }
const user = new User(); const user = new User();
...@@ -40,6 +44,8 @@ describe('Aragami.', () => { ...@@ -40,6 +44,8 @@ describe('Aragami.', () => {
const tmpUser = await aragami.set(User, { name: 'Nanahira', age: 17 }); const tmpUser = await aragami.set(User, { name: 'Nanahira', age: 17 });
expect(tmpUser).toEqual({ name: 'Nanahira', age: 17 }); expect(tmpUser).toEqual({ name: 'Nanahira', age: 17 });
expect(tmpUser).toBeInstanceOf(User); expect(tmpUser).toBeInstanceOf(User);
expect(tmpUser.getName()).resolves.toBe('n.Nanahira');
expect(tmpUser.getAge()).toBe(17);
await expect(aragami.del(tmpUser)).resolves.toBeTruthy(); await expect(aragami.del(tmpUser)).resolves.toBeTruthy();
const userGet = await aragami.get(User, 'n.John'); const userGet = await aragami.get(User, 'n.John');
expect(userGet).toEqual({ name: 'John', age: 30 }); expect(userGet).toEqual({ name: 'John', age: 30 });
......
import { CastType } from '../src/decorators';
import { wrapClass } from '../src/utility/encode-decode';
class Address {
street: string;
city: string;
formatAddress() {
return `${this.street}, ${this.city}`;
}
}
class User {
name: string;
@CastType(() => Address)
address: Address;
@CastType(() => Date)
createdAt: Date;
@CastType(() => Address)
addresses: Address[];
}
describe('wrapClass', () => {
it('should instantiate nested classes with CastType', () => {
const input = {
name: 'John',
address: {
street: '123 St',
city: 'NYC',
},
createdAt: '2023-01-01T00:00:00.000Z',
addresses: [
{ street: 'Street 1', city: 'City 1' },
{ street: 'Street 2', city: 'City 2' },
],
};
const result = wrapClass(User, input);
expect(result).toBeInstanceOf(User);
expect(result.address).toBeInstanceOf(Address);
expect(result.createdAt).toBeInstanceOf(Date);
expect(result.addresses[0]).toBeInstanceOf(Address);
expect(result.address.street).toBe('123 St');
expect(result.addresses[1].city).toBe('City 2');
expect(result.createdAt.toISOString()).toBe('2023-01-01T00:00:00.000Z');
expect(result.address.formatAddress()).toBe('123 St, NYC');
expect(result.addresses[0].formatAddress()).toBe('Street 1, City 1');
expect(result.addresses[1].formatAddress()).toBe('Street 2, City 2');
});
it('should return the same instance if already an instance of the class', () => {
const user = new User();
user.name = 'Existing';
const result = wrapClass(User, user);
expect(result).toBe(user);
});
it('should handle null or undefined gracefully', () => {
expect(wrapClass(User, null)).toBeNull();
expect(wrapClass(User, undefined)).toBeUndefined();
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment