Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Entity.getFieldValue/setFieldValue methods #1027

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 34 additions & 17 deletions packages/codegen/src/generateEntityCodegenFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
newChangesProxy,
newRequiredRule,
setField,
setFieldValue,
setOpts,
toIdOf,
} from "./symbols";
Expand Down Expand Up @@ -401,11 +402,13 @@ export function generateEntityCodegenFile(config: Config, dbMeta: DbMetadata, me
const baseEntity = dbMeta.entities.find((e) => e.name === meta.baseClassName);
const subEntities = dbMeta.entities.filter((e) => e.baseClassName === meta.name);
const base = baseEntity?.entity.type ?? code`${BaseEntity}<${EntityManager}, ${idType}>`;
const maybeBaseFields = baseEntity ? code`extends ${imp('t:' + baseEntity.name + "Fields@./entities.ts")}` : "";
const maybeBaseFields = baseEntity ? code`extends ${imp("t:" + baseEntity.name + "Fields@./entities.ts")}` : "";
const maybeBaseOpts = baseEntity ? code`extends ${baseEntity.entity.optsType}` : "";
const maybeBaseIdOpts = baseEntity ? code`extends ${imp('t:' + baseEntity.name + "IdsOpts@./entities.ts")}` : "";
const maybeBaseFilter = baseEntity ? code`extends ${imp('t:' + baseEntity.name + "Filter@./entities.ts")}` : "";
const maybeBaseGqlFilter = baseEntity ? code`extends ${imp('t:' + baseEntity.name + "GraphQLFilter@./entities.ts")}` : "";
const maybeBaseIdOpts = baseEntity ? code`extends ${imp("t:" + baseEntity.name + "IdsOpts@./entities.ts")}` : "";
const maybeBaseFilter = baseEntity ? code`extends ${imp("t:" + baseEntity.name + "Filter@./entities.ts")}` : "";
const maybeBaseGqlFilter = baseEntity
? code`extends ${imp("t:" + baseEntity.name + "GraphQLFilter@./entities.ts")}`
: "";
const maybeBaseOrder = baseEntity ? code`extends ${baseEntity.entity.orderType}` : "";
const maybeBaseId = baseEntity ? code` & Flavor<${idType}, "${baseEntity.name}">` : "";
const maybePreventBaseTypeInstantiation = meta.abstract
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
): ${entityName}Fields[K]["value"] {
return ${getField}(this as any, key);
}

setFieldValue<K extends keyof ${entityName}Fields>(
key: K,
value: ${entityName}Fields[K]["value"],
): void {
${setFieldValue}(this, key, value);
}

set(opts: Partial<${entityName}Opts>): void {
${setOpts}(this as any as ${entityName}, opts);
${setOpts}(this as any, opts);
}

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 Expand Up @@ -701,33 +717,34 @@ function generateOptsFields(config: Config, meta: EntityDbMetadata): Code[] {

// Make our fields type
function generateFieldsType(config: Config, meta: EntityDbMetadata): Code[] {
const id = code`id: { kind: "primitive"; type: ${meta.primaryKey.fieldType}; unique: ${true}; nullable: never };`;
const id = code`id: { kind: "primitive"; type: ${meta.primaryKey.fieldType}; unique: ${true}; nullable: never; value: never };`;
const primitives = meta.primitives.map((field) => {
const { fieldName, fieldType, notNull, unique, derived } = field;
return code`${fieldName}: { kind: "primitive"; type: ${fieldType}; unique: ${unique}; nullable: ${undefinedOrNever(
notNull,
)}, derived: ${derived !== false} };`;
const uOrNever = undefinedOrNever(notNull);
return code`${fieldName}: { kind: "primitive"; type: ${fieldType}; unique: ${unique}; nullable: ${uOrNever}; value: ${fieldType} | ${uOrNever}; derived: ${derived !== false} };`;
});
const enums = meta.enums.map((field) => {
const { fieldName, enumType, notNull, isArray } = field;
if (isArray) {
// Arrays are always optional and we'll default to `[]`
return code`${fieldName}: { kind: "enum"; type: ${enumType}[]; nullable: never };`;
return code`${fieldName}: { kind: "enum"; type: ${enumType}[]; nullable: never; value: never };`;
} else {
return code`${fieldName}: { kind: "enum"; type: ${enumType}; nullable: ${undefinedOrNever(notNull)} };`;
const uOrNever = undefinedOrNever(notNull);
return code`${fieldName}: { kind: "enum"; type: ${enumType}; nullable: ${uOrNever}; value: ${enumType} | ${uOrNever} };`;
}
});
const pgEnums = meta.pgEnums.map(({ fieldName, enumType, notNull }) => {
const nullable = undefinedOrNever(notNull);
return code`${fieldName}: { kind: "enum"; type: ${enumType}; nullable: ${nullable}; native: true };`;
return code`${fieldName}: { kind: "enum"; type: ${enumType}; nullable: ${nullable}; native: true; value: never };`;
});
const m2o = meta.manyToOnes.map(({ fieldName, otherEntity, notNull, derived }) => {
return code`${fieldName}: { kind: "m2o"; type: ${otherEntity.type}; nullable: ${undefinedOrNever(
notNull,
)}, derived: ${derived !== false} };`;
const uOrNever = undefinedOrNever(notNull);
return code`${fieldName}: { kind: "m2o"; type: ${otherEntity.type}; nullable: ${uOrNever}; value: ${otherEntity.idType} | ${uOrNever}; derived: ${derived !== false} };`;
});
const polys = meta.polymorphics.map(({ fieldName, notNull, fieldType }) => {
return code`${fieldName}: { kind: "poly"; type: ${fieldType}; nullable: ${undefinedOrNever(notNull)} };`;
const uOrNever = undefinedOrNever(notNull);
const genericIdType = config.idType === "number" ? "number" : "string";
return code`${fieldName}: { kind: "poly"; type: ${fieldType}; nullable: ${uOrNever}; value: ${genericIdType} | ${uOrNever}; }`;
});
return [id, ...primitives, ...enums, ...pgEnums, ...m2o, ...polys];
}
Expand Down
5 changes: 4 additions & 1 deletion packages/codegen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ export async function generateAndSaveFiles(config: Config, dbMeta: DbMetadata):
toolName: "joist-codegen",
directory: config.entitiesDirectory,
files,
toStringOpts: { importExtensions: config.esm ? 'js' : false }
toStringOpts: {
dprintOptions: { lineWidth: 150 },
importExtensions: config.esm ? "js" : false,
},
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/codegen/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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 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,6 +74,7 @@ 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");
export const setFieldValue = imp("setFieldValue@joist-orm");
export const New = imp("t:New@joist-orm");
export const DeepNew = imp("t:DeepNew@joist-orm");
export const FactoryOpts = imp("t:FactoryOpts@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): unknown;
setFieldValue(fieldName: string, value: unknown): void;
set(opts: Partial<OptsOf<this>>): void;
setPartial(values: PartialOrNull<OptsOf<this>>): void;
/**
Expand Down
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"
}
35 changes: 19 additions & 16 deletions packages/tests/esm-misc/src/entities/codegen/AuthorCodegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
newChangesProxy,
newRequiredRule,
setField,
setFieldValue,
setOpts,
toIdOf,
} from "joist-orm";
Expand Down Expand Up @@ -42,12 +43,12 @@ import type { BookId, Entity } from "../entities.js";
export type AuthorId = Flavor<string, Author>;

export interface AuthorFields {
id: { kind: "primitive"; type: number; unique: true; nullable: never };
firstName: { kind: "primitive"; type: string; unique: false; nullable: never; derived: false };
lastName: { kind: "primitive"; type: string; unique: false; nullable: undefined; derived: false };
delete: { kind: "primitive"; type: boolean; unique: false; nullable: undefined; derived: false };
createdAt: { kind: "primitive"; type: Date; unique: false; nullable: never; derived: true };
updatedAt: { kind: "primitive"; type: Date; unique: false; nullable: never; derived: true };
id: { kind: "primitive"; type: number; unique: true; nullable: never; value: never };
firstName: { kind: "primitive"; type: string; unique: false; nullable: never; value: string | never; derived: false };
lastName: { kind: "primitive"; type: string; unique: false; nullable: undefined; value: string | undefined; derived: false };
delete: { kind: "primitive"; type: boolean; unique: false; nullable: undefined; value: boolean | undefined; derived: false };
createdAt: { kind: "primitive"; type: Date; unique: false; nullable: never; value: Date | never; derived: true };
updatedAt: { kind: "primitive"; type: Date; unique: false; nullable: never; value: Date | never; derived: true };
}

export interface AuthorOpts {
Expand Down Expand Up @@ -163,12 +164,20 @@ export abstract class AuthorCodegen extends BaseEntity<EntityManager, string> im
return getField(this, "updatedAt");
}

getFieldValue<K extends keyof AuthorFields>(key: K): AuthorFields[K]["value"] {
return getField(this as any, key);
}

setFieldValue<K extends keyof AuthorFields>(key: K, value: AuthorFields[K]["value"]): void {
setFieldValue(this, key, value);
}

set(opts: Partial<AuthorOpts>): void {
setOpts(this as any as Author, opts);
setOpts(this as any, opts);
}

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 All @@ -182,14 +191,8 @@ export abstract class AuthorCodegen extends BaseEntity<EntityManager, string> im
populate<H extends LoadHint<Author>>(hint: H): Promise<Loaded<Author, H>>;
populate<H extends LoadHint<Author>>(opts: { hint: H; forceReload?: boolean }): Promise<Loaded<Author, H>>;
populate<H extends LoadHint<Author>, V>(hint: H, fn: (a: Loaded<Author, H>) => V): Promise<V>;
populate<H extends LoadHint<Author>, V>(
opts: { hint: H; forceReload?: boolean },
fn: (a: Loaded<Author, H>) => V,
): Promise<V>;
populate<H extends LoadHint<Author>, V>(
hintOrOpts: any,
fn?: (a: Loaded<Author, H>) => V,
): Promise<Loaded<Author, H> | V> {
populate<H extends LoadHint<Author>, V>(opts: { hint: H; forceReload?: boolean }, fn: (a: Loaded<Author, H>) => V): Promise<V>;
populate<H extends LoadHint<Author>, V>(hintOrOpts: any, fn?: (a: Loaded<Author, H>) => V): Promise<Loaded<Author, H> | V> {
return this.em.populate(this as any as Author, hintOrOpts, fn);
}

Expand Down
24 changes: 15 additions & 9 deletions packages/tests/esm-misc/src/entities/codegen/BookCodegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
newChangesProxy,
newRequiredRule,
setField,
setFieldValue,
setOpts,
toIdOf,
} from "joist-orm";
Expand Down Expand Up @@ -40,9 +41,9 @@ import type { AuthorId, AuthorOrder, Entity } from "../entities.js";
export type BookId = Flavor<string, Book>;

export interface BookFields {
id: { kind: "primitive"; type: number; unique: true; nullable: never };
title: { kind: "primitive"; type: string; unique: false; nullable: never; derived: false };
author: { kind: "m2o"; type: Author; nullable: never; derived: false };
id: { kind: "primitive"; type: number; unique: true; nullable: never; value: never };
title: { kind: "primitive"; type: string; unique: false; nullable: never; value: string | never; derived: false };
author: { kind: "m2o"; type: Author; nullable: never; value: AuthorId | never; derived: false };
}

export interface BookOpts {
Expand Down Expand Up @@ -120,12 +121,20 @@ export abstract class BookCodegen extends BaseEntity<EntityManager, string> impl
setField(this, "title", cleanStringValue(title));
}

getFieldValue<K extends keyof BookFields>(key: K): BookFields[K]["value"] {
return getField(this as any, key);
}

setFieldValue<K extends keyof BookFields>(key: K, value: BookFields[K]["value"]): void {
setFieldValue(this, key, value);
}

set(opts: Partial<BookOpts>): void {
setOpts(this as any as Book, opts);
setOpts(this as any, opts);
}

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 All @@ -139,10 +148,7 @@ export abstract class BookCodegen extends BaseEntity<EntityManager, string> impl
populate<H extends LoadHint<Book>>(hint: H): Promise<Loaded<Book, H>>;
populate<H extends LoadHint<Book>>(opts: { hint: H; forceReload?: boolean }): Promise<Loaded<Book, H>>;
populate<H extends LoadHint<Book>, V>(hint: H, fn: (b: Loaded<Book, H>) => V): Promise<V>;
populate<H extends LoadHint<Book>, V>(
opts: { hint: H; forceReload?: boolean },
fn: (b: Loaded<Book, H>) => V,
): Promise<V>;
populate<H extends LoadHint<Book>, V>(opts: { hint: H; forceReload?: boolean }, fn: (b: Loaded<Book, H>) => V): Promise<V>;
populate<H extends LoadHint<Book>, V>(hintOrOpts: any, fn?: (b: Loaded<Book, H>) => V): Promise<Loaded<Book, H> | V> {
return this.em.populate(this as any as Book, hintOrOpts, fn);
}
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"
}