Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenh committed Apr 11, 2024
1 parent ca31dd4 commit b098fb1
Show file tree
Hide file tree
Showing 54 changed files with 1,162 additions and 132 deletions.
20 changes: 18 additions & 2 deletions packages/codegen/src/generateEntityCodegenFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
EntityGraphQLFilter,
EntityManager,
EntityMetadata,
FieldType,
FieldsOf,
FilterOf,
Flavor,
Expand All @@ -44,6 +45,7 @@ import {
ReactiveField,
ReactiveReference,
SSAssert,
SettableFields,
TaggedId,
ValueFilter,
ValueGraphQLFilter,
Expand All @@ -62,6 +64,7 @@ import {
hasOneToOne,
isEntity,
isLoaded,
setFieldValue,
loadLens,
mustBeSubType,
newChangesProxy,
Expand Down Expand Up @@ -527,12 +530,25 @@ export function generateEntityCodegenFile(config: Config, dbMeta: DbMetadata, me
${primitives}
getFieldValue<K extends keyof ${entityName}Fields>(
key: K
): ${FieldType}<${entityName}Fields, K> {
return ${getField}(this as any, key);
}
setFieldValue<K extends keyof ${SettableFields}<${entityName}Fields> & keyof ${entityName}Fields>(
key: K,
value: ${FieldType}<${entityName}Fields, K>,
): void {
${setFieldValue}(this, key, value);
}
set(opts: Partial<${entityName}Opts>): void {
${setOpts}(this as any as ${entityName}, opts);
${setOpts}(this as any, optsOrKey);
}
setPartial(opts: ${PartialOrNull}<${entityName}Opts>): void {
${setOpts}(this as any as ${entityName}, opts as ${OptsOf}<${entityName}>, { partial: true });
${setOpts}(this as any, opts as ${OptsOf}<${entityName}>, { partial: true });
}
get changes(): ${Changes}<${entityName}${maybeOtherTypeChanges}> {
Expand Down
13 changes: 13 additions & 0 deletions packages/codegen/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const PolymorphicKeySerde = imp("PolymorphicKeySerde@joist-orm");
export const PrimitiveSerde = imp("PrimitiveSerde@joist-orm");
export const BigIntSerde = imp("BigIntSerde@joist-orm");
export const JsonSerde = imp("JsonSerde@joist-orm");
export const SettableFields = imp("SettableFields@joist-orm");
export const FieldType = imp("FieldType@joist-orm");
export const SuperstructSerde = imp("SuperstructSerde@joist-orm");
export const TaggedId = imp("t:TaggedId@joist-orm");
export const ZodSerde = imp("ZodSerde@joist-orm");
Expand Down Expand Up @@ -73,9 +75,20 @@ export const hasOneToOne = imp("hasOneToOne@joist-orm");
export const hasManyToMany = imp("hasManyToMany@joist-orm");
export const hasLargeManyToMany = imp("hasLargeManyToMany@joist-orm");
export const newTestInstance = imp("newTestInstance@joist-orm");
<<<<<<< HEAD
export const New = imp("t:New@joist-orm");
export const DeepNew = imp("t:DeepNew@joist-orm");
export const FactoryOpts = imp("t:FactoryOpts@joist-orm");
||||||| parent of 58b3b20a (Rename to getFieldValue/setFieldValue.)
export const New = imp("New@joist-orm");
export const DeepNew = imp("DeepNew@joist-orm");
export const FactoryOpts = imp("FactoryOpts@joist-orm");
=======
export const setFieldValue = imp("setFieldValue@joist-orm");
export const New = imp("New@joist-orm");
export const DeepNew = imp("DeepNew@joist-orm");
export const FactoryOpts = imp("FactoryOpts@joist-orm");
>>>>>>> 58b3b20a (Rename to getFieldValue/setFieldValue.)
export const SSAssert = imp("assert@superstruct");
export const Zod = imp("z@zod");
export const EntityConstructor = imp("t:EntityConstructor@joist-orm");
Expand Down
5 changes: 5 additions & 0 deletions packages/orm/src/BaseEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getField } from "./fields";
import {
Entity,
EntityManager,
FieldsOf,
InstanceData,
OptsOf,
PartialOrNull,
Expand Down Expand Up @@ -70,6 +71,10 @@ export abstract class BaseEntity<EM extends EntityManager, I extends IdType = Id
return deTagId(getMetadata(this), this.id);
}

abstract getFieldValue(key: string): any;

abstract setFieldValue(fieldName: string, value: unknown): void;

abstract set(values: Partial<OptsOf<Entity>>): void;

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/orm/src/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EntityManager, OptsOf, TaggedId } from "./EntityManager";
import { EntityManager, FieldsOf, OptsOf, TaggedId } from "./EntityManager";
import { BaseEntity, PartialOrNull } from "./index";

export function isEntity(maybeEntity: unknown): maybeEntity is Entity {
Expand All @@ -19,6 +19,8 @@ export interface Entity {
readonly isNewEntity: boolean;
readonly isDeletedEntity: boolean;
readonly isDirtyEntity: boolean;
getFieldValue(fieldName: string): any;
setFieldValue(fieldName: string, value: unknown): void;
set(opts: Partial<OptsOf<this>>): void;
setPartial(values: PartialOrNull<OptsOf<this>>): void;
/**
Expand Down
10 changes: 10 additions & 0 deletions packages/orm/src/EntityFields.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IdOf } from "./EntityManager";

/** All the fields for an entity in the `FieldsOf` / `EntityField` format. */
export type EntityFields<T> = {
[K in keyof T]: EntityField;
Expand All @@ -19,3 +21,11 @@ export type SettableFields<F> = {
? F[K]
: never;
};

export type FieldType<F, K extends keyof F> = F[K] extends { kind: "primitive"; type: infer T; nullable: infer N }
? T | N
: F[K] extends { kind: "enum"; type: infer T; nullable: infer N }
? T | N
: F[K] extends { kind: "m2o"; type: infer E; nullable: infer N }
? IdOf<E> | N
: never;
18 changes: 16 additions & 2 deletions packages/orm/src/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@ import { getInstanceData } from "./BaseEntity";
import { Entity, isEntity } from "./Entity";
import { getEmInternalApi } from "./EntityManager";
import { getMetadata } from "./EntityMetadata";
import { ensureNotDeleted, maybeResolveReferenceToId } from "./index";
import {ensureNotDeleted, fail, isManyToOneReference, maybeResolveReferenceToId} from "./index";

/**
* Sets the current value of `fieldName` to `value`, while also ensuring that any relations
* are properly set.
*/
export function setFieldValue(entity: Entity, fieldName: string, value: any): void {
getField(entity, fieldName);
const maybeRef = (entity as any)[fieldName];
if (isManyToOneReference(maybeRef)) {
maybeRef.set(value);
} else {
setField(entity, fieldName, value);
}
}

/**
* Returns the current value of `fieldName`, this is an internal method that should
Expand All @@ -17,7 +31,7 @@ export function getField(entity: Entity, fieldName: string): any {
if (fieldName in data) {
return data[fieldName];
} else {
const serde = getMetadata(entity).allFields[fieldName].serde ?? fail(`Missing serde for ${fieldName}`);
const serde = getMetadata(entity).allFields[fieldName]?.serde ?? fail(`Invalid field ${fieldName}`);
serde.setOnEntity(data, row);
return data[fieldName];
}
Expand Down
2 changes: 1 addition & 1 deletion packages/orm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export { ConfigApi, EntityHook } from "./config";
export { configureMetadata, getConstructorFromTaggedId, maybeGetConstructorFromReference } from "./configure";
export { DeepPartialOrNull } from "./createOrUpdatePartial";
export * from "./drivers";
export { getField, isChangeableField, isFieldSet, setField } from "./fields";
export { getField, isChangeableField, isFieldSet, setField, setFieldValue } from "./fields";
export * from "./getProperties";
export * from "./keys";
export { kq, kqDot, kqStar } from "./keywords";
Expand Down
2 changes: 1 addition & 1 deletion packages/tests/esm-misc/joist-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"createdAt": { "names": ["created_at", "createdAt"], "required": false },
"updatedAt": { "names": ["updated_at", "updatedAt"], "required": false }
},
"version": "1.155.2"
"version": "1.156.0"
}
29 changes: 26 additions & 3 deletions packages/tests/esm-misc/src/entities/codegen/AuthorCodegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import {
cleanStringValue,
ConfigApi,
failNoIdYet,
FieldType,
getField,
getInstanceData,
hasMany,
isLoaded,
isManyToOneReference,
loadLens,
newChangesProxy,
newRequiredRule,
setField,
setOpts,
SettableFields,
toIdOf,
} from "joist-orm";
import type {
Expand Down Expand Up @@ -163,12 +166,32 @@ export abstract class AuthorCodegen extends BaseEntity<EntityManager, string> im
return getField(this, "updatedAt");
}

set(opts: Partial<AuthorOpts>): void {
setOpts(this as any as Author, opts);
get<K extends keyof AuthorFields>(key: K): FieldType<AuthorFields, K> {
return getField(this as any, key);
}

set<K extends keyof SettableFields<AuthorFields> & keyof AuthorFields>(
key: K,
value: FieldType<AuthorFields, K>,
): void;
set(opts: Partial<AuthorOpts>): void;
set(optsOrKey: string | Partial<AuthorOpts>, value?: unknown): void {
if (typeof optsOrKey === "string") {
// make sure we're initialized
getField(this, optsOrKey);
const maybeRef = (this as any)[optsOrKey];
if (isManyToOneReference(maybeRef)) {
maybeRef.set(value);
} else {
setField(this as any, optsOrKey, value);
}
} else {
setOpts(this as any, optsOrKey);
}
}

setPartial(opts: PartialOrNull<AuthorOpts>): void {
setOpts(this as any as Author, opts as OptsOf<Author>, { partial: true });
setOpts(this as any, opts as OptsOf<Author>, { partial: true });
}

get changes(): Changes<Author> {
Expand Down
26 changes: 23 additions & 3 deletions packages/tests/esm-misc/src/entities/codegen/BookCodegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import {
cleanStringValue,
ConfigApi,
failNoIdYet,
FieldType,
getField,
getInstanceData,
hasOne,
isLoaded,
isManyToOneReference,
loadLens,
newChangesProxy,
newRequiredRule,
setField,
setOpts,
SettableFields,
toIdOf,
} from "joist-orm";
import type {
Expand Down Expand Up @@ -120,12 +123,29 @@ export abstract class BookCodegen extends BaseEntity<EntityManager, string> impl
setField(this, "title", cleanStringValue(title));
}

set(opts: Partial<BookOpts>): void {
setOpts(this as any as Book, opts);
get<K extends keyof BookFields>(key: K): FieldType<BookFields, K> {
return getField(this as any, key);
}

set<K extends keyof SettableFields<BookFields> & keyof BookFields>(key: K, value: FieldType<BookFields, K>): void;
set(opts: Partial<BookOpts>): void;
set(optsOrKey: string | Partial<BookOpts>, value?: unknown): void {
if (typeof optsOrKey === "string") {
// make sure we're initialized
getField(this, optsOrKey);
const maybeRef = (this as any)[optsOrKey];
if (isManyToOneReference(maybeRef)) {
maybeRef.set(value);
} else {
setField(this as any, optsOrKey, value);
}
} else {
setOpts(this as any, optsOrKey);
}
}

setPartial(opts: PartialOrNull<BookOpts>): void {
setOpts(this as any as Book, opts as OptsOf<Book>, { partial: true });
setOpts(this as any, opts as OptsOf<Book>, { partial: true });
}

get changes(): Changes<Book> {
Expand Down
2 changes: 1 addition & 1 deletion packages/tests/integration/joist-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,5 @@
}
},
"entitiesDirectory": "./src/entities",
"version": "1.155.2"
"version": "1.156.0"
}
82 changes: 82 additions & 0 deletions packages/tests/integration/src/Fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Author, LargePublisher } from "src/entities";
import { insertAuthor, insertLargePublisher, insertPublisher, select } from "src/entities/inserts";
import { newEntityManager } from "src/testEm";

describe("Fields", () => {
it("can use get/set to copy values", async () => {
// Given an existing author
await insertPublisher({ name: "p" });
await insertAuthor({ first_name: "f", publisher_id: 1 });
const em = newEntityManager();
const a1 = await em.load(Author, "a:1");
// And we create a new author
const a2 = em.create(Author, { firstName: "b" });
// When we iterate for both primitive fields & relations
for (const fieldName of ["firstName", "publisher"] as const) {
// And use get/set to copy the value, even if the relation isn't loaded
a2.setFieldValue(fieldName, a1.getFieldValue(fieldName));
}
await em.flush();
// Then it worked
expect((await select("authors"))[1]).toMatchObject({
first_name: "f",
publisher_id: 1,
});
});

it("can use get on base or subtype fields", async () => {
// Given we have a CTI subtype entity
await insertLargePublisher({ name: "p" });
const em = newEntityManager();
const p1 = await em.load(LargePublisher, "p:1");
// Then we can get fields from the subtype
expect(p1.getFieldValue("country")).toBe("country");
// Or the base type
expect(p1.getFieldValue("name")).toBe("p");
// And not fields from the other subtype
// @ts-expect-error
expect(() => p1.getFieldValue("city")).toThrow("Invalid field city");
});

it("setting derived fields causes a type error", async () => {
// Given an existing author
await insertAuthor({ first_name: "f" });
const em = newEntityManager();
const a1 = await em.load(Author, "a:1");
// When we try to set the num of books
a1.setFieldValue("numberOfBooks", 1);
await em.flush();
// Then it worked
expect((await select("authors"))[0]).toMatchObject({
first_name: "f",
number_of_books: 1,
});
});

it("can use get on loaded relations", async () => {
// Given an existing author
await insertPublisher({ name: "p" });
await insertAuthor({ first_name: "f", publisher_id: 1 });
const em = newEntityManager();
const a1 = await em.load(Author, "a:1");
expect(a1.getFieldValue("publisher")).toBe("p:1");
});

it("can use set on loaded relations", async () => {
// Given an existing author
await insertPublisher({ name: "p1" });
await insertPublisher({ id: 2, name: "p2" });
await insertAuthor({ first_name: "f", publisher_id: 1 });
const em = newEntityManager();
const a1 = await em.load(Author, "a:1", "publisher");
expect(a1.publisher.id).toBe("p:1");
a1.setFieldValue("publisher", "p:2");
expect(a1.publisher.id).toBe("p:2");
expect(a1.publisher.isLoaded).toBe(false);
await em.flush();
expect((await select("authors"))[0]).toMatchObject({
first_name: "f",
publisher_id: 2,
});
});
});
Loading

0 comments on commit b098fb1

Please sign in to comment.