Skip to content

Commit

Permalink
Merge pull request #7 from koyopro/feature/to_json
Browse files Browse the repository at this point in the history
Serialization
  • Loading branch information
koyopro authored Jun 2, 2024
2 parents 70714a7 + bfb7e93 commit 43665c6
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Accella Project

- [Accel Record](./packages/accel-record/)
- [Accel Record Factory](./packages/accel-record-factory/)
2 changes: 2 additions & 0 deletions packages/accel-record-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Fields } from "./fields.js";
import { ModelMeta } from "./meta.js";
import { Dirty } from "./model/dirty.js";
import { Import } from "./model/import.js";
import { Serialization } from "./model/serialization.js";
import { Validations } from "./model/validations.js";
import { Persistence } from "./persistence.js";
import { Query } from "./query.js";
Expand Down Expand Up @@ -63,6 +64,7 @@ export class Model extends classIncludes(
Import,
Persistence,
Query,
Serialization,
Transaction,
Validations
) {
Expand Down
156 changes: 156 additions & 0 deletions packages/accel-record-core/src/model/serialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { BelongsToAssociation } from "../associations/belongsToAssociation.js";
import { Collection } from "../associations/collectionProxy.js";
import { HasManyAssociation } from "../associations/hasManyAssociation.js";
import { HasOneAssociation } from "../associations/hasOneAssociation.js";
import { Model } from "../index.js";
import { Meta } from "../meta.js";

type RetriveModel<T> = undefined extends T
? NonNullable<T>
: T extends Collection<infer U, infer S>
? U
: T;

type ToUnion<T extends any[] | undefined> = undefined extends T
? never
: NonNullable<T>[number];

type ToHashIncludeResult<K, T, O extends ToHashOptions<any>> = K extends keyof T
? undefined extends T[K]
? ToHashResult<NonNullable<T[K]>, O> | undefined // HasOne
: T[K] extends Collection<infer U, infer S>
? ToHashResult<U, O>[] // HasMany
: ToHashResult<T[K], O> // BelongsTo
: {};

type ToHashInclude<O extends ToHashOptions<T>, T> = O["include"] extends string
? {
[K in O["include"]]: ToHashIncludeResult<K, T, {}>;
}
: O["include"] extends ToHashIncludeOption<T>
? {
[K in keyof O["include"]]: ToHashIncludeResult<
K,
T,
// @ts-ignore
NonNullable<O["include"][K]>
>;
}
: {};

type ToHashMethods<
O extends ToHashOptions<T>,
T,
> = undefined extends O["methods"]
? {}
: {
[K in ToUnion<O["methods"]>]: T[Extract<K, keyof T>] extends () => any
? ReturnType<T[Extract<K, keyof T>]>
: never;
};

export type ToHashResult<T, O extends ToHashOptions<T>> = {
[K in undefined extends O["only"]
? Exclude<keyof Meta<T>["OrderInput"], ToUnion<O["except"]>>
: ToUnion<O["only"]>]: T[Extract<K, keyof T>];
} & ToHashInclude<O, T> &
ToHashMethods<O, T>;

type ToHashIncludeOption<T> = {
[K in Meta<T>["AssociationKey"]]?: K extends keyof T
? ToHashOptions<RetriveModel<T[K]>>
: never;
};

type NoArgMethods<T> = {
[K in keyof T]: T[K] extends () => any ? K : never;
}[keyof T];

export type ToHashOptions<T> = {
only?: (keyof Meta<T>["OrderInput"])[];
except?: (keyof Meta<T>["OrderInput"])[];
methods?: NoArgMethods<T>[];
include?: Meta<T>["AssociationKey"] | ToHashIncludeOption<T>;
};

/**
* Represents a Serialization class that provides methods for converting a model instance to a hash object.
*
* This class is intended to be inherited by the Model class.
*/
export class Serialization {
/**
* Converts the model instance to a hash object.
* @param options - The options for the conversion.
* @returns The hash object representing the model instance.
*/
toHash<T, O extends ToHashOptions<T>>(
this: T,
options?: O
): ToHashResult<T, O>;
toHash<T extends Model, O extends ToHashOptions<T>>(
this: T,
options: O = {} as O
) {
const ret = {} as any;
for (const field of this.columnFields) {
if (options.only && !options.only.includes(field.name)) continue;
if (options.except && options.except.includes(field.name)) continue;

ret[field.name] = this[field.dbName as keyof T] ?? undefined;
}
if (typeof options.include === "string") {
ret[options.include] = this.toHashInclude(options.include, {});
}
if (typeof options.include === "object") {
for (const [key, value] of Object.entries(options.include)) {
ret[key] = this.toHashInclude(key, value ?? {});
}
}
for (const method of options.methods ?? []) {
const f = this[method as keyof T];
if (typeof f !== "function") continue;
ret[method] = f.bind(this)();
}
return ret;
}

/**
* Converts the associated model instance to a hash object.
* @param key - The key of the association.
* @param options - The options for the conversion.
* @returns The hash object representing the associated model instance.
*/
protected toHashInclude<T extends Model>(
this: T,
key: string,
options: ToHashOptions<any>
) {
const association = this.associations.get(key);
if (association instanceof HasManyAssociation) {
return (this[key as keyof T] as any).map((r: Model) => r.toHash(options));
}
if (
association instanceof HasOneAssociation ||
association instanceof BelongsToAssociation
) {
return (this[key as keyof T] as Model)?.toHash(options);
}
return undefined;
}

/**
* Converts the model instance to a JSON string representation.
*
* @typeparam T - The type of the model.
* @typeparam O - The type of the options for serialization.
* @param options - The options for serialization.
* @returns The JSON string representation of the model instance.
*/
toJson<T extends Model, O extends ToHashOptions<T>>(
this: T,
options?: O
): string {
return JSON.stringify(this.toHash(options));
}
}
1 change: 1 addition & 0 deletions packages/accel-record-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class Query {
...attributes: F
// @ts-ignore
): Relation<{ [K in F[number]]: InstanceType<T>[K] }, Meta<T>> {
// @ts-ignore
return this.all().select(...attributes);
}

Expand Down
29 changes: 29 additions & 0 deletions packages/accel-record-core/src/relation/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Model } from "../index.js";
import { type ModelMeta } from "../meta.js";
import { ToHashOptions, ToHashResult } from "../model/serialization.js";
import { classIncludes } from "../utils.js";
import { Association } from "./association.js";
import { RelationBase } from "./base.js";
Expand Down Expand Up @@ -41,6 +42,34 @@ export class Relation<T, M extends ModelMeta> extends classIncludes(
return (this.cache ||= this.load());
}

/**
* Converts the relation to an array of hash objects.
*
* @param options - The options for the conversion.
* @returns An array of hash objects.
*/
toHashArray<O extends ToHashOptions<T>>(
options?: O
): T extends Model ? ToHashResult<T, O>[] : never;
toHashArray(options = {}): Record<string, any>[] {
return this.toArray().map((row) =>
row instanceof Model ? row.toHash(options) : {}
);
}

/**
* Converts the relation to a JSON string representation.
*
* @param options - The options for the conversion.
* @returns A JSON string representation of the relation.
*/
toJson<O extends ToHashOptions<T>>(
options?: O
): T extends Model ? string : never;
toJson(options = {}): string {
return JSON.stringify(this.toHashArray(options));
}

/**
* Applies a function to each element in the relation and returns an array of the results.
*
Expand Down
40 changes: 40 additions & 0 deletions packages/accel-record/README-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,46 @@ class MyValidator extends Validator<{ key: string | undefined }> {
}
```
## Serialization
`toHash`メソッドや`toHashArray`メソッドを利用することで、モデルのデータをプレーンなオブジェクトに変換することができます。
```ts
import { User } from "./models/index.js";

const userHash = User.first()!.toHash({
only: ["firstName", "lastName"],
include: { posts: { only: ["title"] } },
});
console.log(userHash);
// => { firstName: "John", lastName: "Doe", posts: [{ title: "Hello" }] }

const usersHashArray = User.all().toHashArray({
only: ["firstName", "lastName"],
});
console.log(usersHashArray);
// => [{ firstName: "John", lastName: "Doe" }, { firstName: "Alice", lastName: "Smith" }]
```
`toJson`メソッドを利用することで、モデルのデータをJSON文字列に変換することができます。
```ts
import { User } from "./models/index.js";

const userJson = User.first()!.toHah({
only: ["firstName", "lastName"],
include: { posts: { only: ["title"] } },
});
console.log(userJson);
// => {"firstName":"John","lastName":"Doe","posts":[{"title":"Hello"}]}

const usersJson = User.all().toHashArray({
only: ["firstName", "lastName"],
});
console.log(usersJson);
// => [{"firstName":"John","lastName":"Doe"},{"firstName":"Alice","lastName":"Smith"}]
```
## Bulk Insert
Bulk Insertは、一度に複数のレコードをデータベースに挿入する機能です。 \
Expand Down
40 changes: 40 additions & 0 deletions packages/accel-record/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,46 @@ class MyValidator extends Validator<{ key: string | undefined }> {
}
```
## Serialization
By using the `toHash` and `toHashArray` methods, you can convert the model's data into plain objects.
```ts
import { User } from "./models/index.js";

const userHash = User.first()!.toHash({
only: ["firstName", "lastName"],
include: { posts: { only: ["title"] } },
});
console.log(userHash);
// => { firstName: "John", lastName: "Doe", posts: [{ title: "Hello" }] }

const usersHashArray = User.all().toHashArray({
only: ["firstName", "lastName"],
});
console.log(usersHashArray);
// => [{ firstName: "John", lastName: "Doe" }, { firstName: "Alice", lastName: "Smith" }]
```
By using the `toJson` method, you can convert the model's data into a JSON string.
```ts
import { User } from "./models/index.js";

const userJson = User.first()!.toHah({
only: ["firstName", "lastName"],
include: { posts: { only: ["title"] } },
});
console.log(userJson);
// => {"firstName":"John","lastName":"Doe","posts":[{"title":"Hello"}]}

const usersJson = User.all().toHashArray({
only: ["firstName", "lastName"],
});
console.log(usersJson);
// => [{"firstName":"John","lastName":"Doe"},{"firstName":"Alice","lastName":"Smith"}]
```
## Bulk Insert
Bulk Insert is a feature that allows you to insert multiple records into the database at once. \
Expand Down
16 changes: 16 additions & 0 deletions tests/models/relation/serialization.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { User } from "..";

test("Relation#toHashArray()", () => {
const users = User.all().toHashArray();
expectTypeOf(users[0]).toEqualTypeOf<{
id: number;
name: string | undefined;
age: number | undefined;
email: string;
createdAt: Date;
updatedAt: Date;
}>();

const withSelect = User.select("id").toHashArray();
expectTypeOf(withSelect).toEqualTypeOf<never>();
});
49 changes: 49 additions & 0 deletions tests/models/relation/serialization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { User } from "..";
import { $user } from "../../factories/user";

const objectContaining = expect.objectContaining;

test("Relation#toHashArray()", () => {
$user.create({ name: "hoge" });
$user.create({ name: "fuga", age: 20 });

const users = User.all().toHashArray();
expect(users).toHaveLength(2);
expect(users[0]).toEqual(
objectContaining({
name: "hoge",
age: undefined,
})
);
expect(users[1]).toEqual(
objectContaining({
name: "fuga",
age: 20,
})
);

expect(User.all().toHashArray({ only: ["name", "age"] })[0]).toEqual({
name: "hoge",
age: undefined,
});
});

test("Relation#toJson()", () => {
const now = new Date("Tue, 01 Dec 2024 10:00:00 GMT");
$user.create({ name: "hoge", createdAt: now });
$user.create({ name: "fuga", createdAt: now });

const result = User.all().toJson({ only: ["name", "createdAt"] });
expect(result).toEqual(
JSON.stringify([
{
name: "hoge",
createdAt: "2024-12-01T10:00:00.000Z",
},
{
name: "fuga",
createdAt: "2024-12-01T10:00:00.000Z",
},
])
);
});
Loading

0 comments on commit 43665c6

Please sign in to comment.