chore(): faster serialization (#13325)
* chore(): Improve serialization perf * better optimization with monomorphic approach and consistent object shape and array operations * cleanup * cleanup * fix * Create short-birds-help.md * ref work * ref work * address feedback * save intermediary changes * save intermediary changes
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Chore/faster serialization
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* This is an optimized mikro orm serializer to create a highly optimized serialization pipeline
|
||||||
|
* that leverages V8's JIT compilation and inline caching mechanisms.
|
||||||
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Collection,
|
Collection,
|
||||||
EntityDTO,
|
EntityDTO,
|
||||||
@@ -9,65 +14,125 @@ import {
|
|||||||
Reference,
|
Reference,
|
||||||
ReferenceKind,
|
ReferenceKind,
|
||||||
SerializationContext,
|
SerializationContext,
|
||||||
SerializeOptions,
|
|
||||||
Utils,
|
Utils,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
|
|
||||||
type CustomSerializeOptions<T, P = any> = SerializeOptions<T, P & string> & {
|
const STATIC_OPTIONS_SHAPE: {
|
||||||
preventCircularRef?: boolean
|
populate: string[] | boolean | undefined
|
||||||
populate?: [keyof T][] | boolean
|
exclude: string[] | undefined
|
||||||
|
preventCircularRef: boolean | undefined
|
||||||
|
skipNull: boolean | undefined
|
||||||
|
ignoreSerializers: boolean | undefined
|
||||||
|
forceObject: boolean | undefined
|
||||||
|
} = {
|
||||||
|
populate: ["*"],
|
||||||
|
exclude: undefined,
|
||||||
|
preventCircularRef: true,
|
||||||
|
skipNull: undefined,
|
||||||
|
ignoreSerializers: undefined,
|
||||||
|
forceObject: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_ARRAY: string[] = []
|
||||||
|
|
||||||
|
const WILDCARD = "*"
|
||||||
|
const DOT = "."
|
||||||
|
const UNDERSCORE = "_"
|
||||||
|
|
||||||
|
// JIT-friendly function with predictable patterns
|
||||||
function isVisible<T extends object>(
|
function isVisible<T extends object>(
|
||||||
meta: EntityMetadata<T>,
|
meta: EntityMetadata<T>,
|
||||||
propName: string,
|
propName: string,
|
||||||
options: CustomSerializeOptions<T> = {}
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
} = STATIC_OPTIONS_SHAPE
|
||||||
): boolean {
|
): boolean {
|
||||||
if (options.populate === true) {
|
// Fast path for boolean populate
|
||||||
|
const populate = options.populate
|
||||||
|
if (populate === true) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (Array.isArray(populate)) {
|
||||||
Array.isArray(options.populate) &&
|
// Check exclusions first (early exit)
|
||||||
options.exclude?.find((item) => item === propName)
|
const exclude = options.exclude
|
||||||
) {
|
if (exclude && exclude.length > 0) {
|
||||||
|
const excludeLen = exclude.length
|
||||||
|
for (let i = 0; i < excludeLen; i++) {
|
||||||
|
if (exclude[i] === propName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoist computations outside loop
|
||||||
|
const propNameLen = propName.length
|
||||||
|
const propPrefix = propName + DOT
|
||||||
|
const propPrefixLen = propPrefix.length
|
||||||
|
const populateLen = populate.length
|
||||||
|
|
||||||
|
// Simple loop that JIT can optimize well
|
||||||
|
for (let i = 0; i < populateLen; i++) {
|
||||||
|
const item = populate[i]
|
||||||
|
if (item === propName || item === WILDCARD) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
item.length > propNameLen &&
|
||||||
|
item.substring(0, propPrefixLen) === propPrefix
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// Inline property check for non-array case
|
||||||
Array.isArray(options.populate) &&
|
|
||||||
(options.populate?.find(
|
|
||||||
(item) => item === propName || item.startsWith(propName + ".")
|
|
||||||
) ||
|
|
||||||
options.populate.includes("*"))
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const prop = meta.properties[propName]
|
const prop = meta.properties[propName]
|
||||||
const visible = (prop && !prop.hidden) || prop === undefined // allow unknown properties
|
const visible = (prop && !prop.hidden) || prop === undefined
|
||||||
const prefixed = prop && !prop.primary && propName.startsWith("_") // ignore prefixed properties, if it's not a PK
|
const prefixed = prop && !prop.primary && propName.charAt(0) === UNDERSCORE
|
||||||
|
|
||||||
return visible && !prefixed
|
return visible && !prefixed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean, JIT-friendly function
|
||||||
function isPopulated<T extends object>(
|
function isPopulated<T extends object>(
|
||||||
entity: T,
|
entity: T,
|
||||||
propName: string,
|
propName: string,
|
||||||
options: CustomSerializeOptions<T>
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
} = STATIC_OPTIONS_SHAPE
|
||||||
): boolean {
|
): boolean {
|
||||||
if (
|
const populate = options.populate
|
||||||
Array.isArray(options.populate) &&
|
|
||||||
(options.populate?.find(
|
// Fast path for boolean
|
||||||
(item) => item === propName || item.startsWith(propName + ".")
|
if (typeof populate === "boolean") {
|
||||||
) ||
|
return populate
|
||||||
options.populate.includes("*"))
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options.populate === "boolean") {
|
if (!Array.isArray(populate)) {
|
||||||
return options.populate
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoist computations for JIT optimization
|
||||||
|
const propNameLen = propName.length
|
||||||
|
const propPrefix = propName + DOT
|
||||||
|
const propPrefixLen = propPrefix.length
|
||||||
|
const populateLen = populate.length
|
||||||
|
|
||||||
|
// Simple predictable loop
|
||||||
|
for (let i = 0; i < populateLen; i++) {
|
||||||
|
const item = populate[i]
|
||||||
|
if (item === propName || item === WILDCARD) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
item.length > propNameLen &&
|
||||||
|
item.substring(0, propPrefixLen) === propPrefix
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -80,6 +145,7 @@ function isPopulated<T extends object>(
|
|||||||
* @param options
|
* @param options
|
||||||
* @param parents
|
* @param parents
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore
|
||||||
function filterEntityPropToSerialize({
|
function filterEntityPropToSerialize({
|
||||||
propName,
|
propName,
|
||||||
meta,
|
meta,
|
||||||
@@ -88,38 +154,50 @@ function filterEntityPropToSerialize({
|
|||||||
}: {
|
}: {
|
||||||
propName: string
|
propName: string
|
||||||
meta: EntityMetadata
|
meta: EntityMetadata
|
||||||
options: CustomSerializeOptions<any>
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
}
|
||||||
parents?: string[]
|
parents?: string[]
|
||||||
}): boolean {
|
}): boolean {
|
||||||
parents ??= []
|
const parentsArray = parents || EMPTY_ARRAY
|
||||||
|
|
||||||
const isVisibleRes = isVisible(meta, propName, options)
|
const isVisibleRes = isVisible(meta, propName, options)
|
||||||
const prop = meta.properties[propName]
|
const prop = meta.properties[propName]
|
||||||
|
|
||||||
// Only prevent circular references if prop is a relation
|
|
||||||
if (
|
if (
|
||||||
prop &&
|
prop &&
|
||||||
options.preventCircularRef &&
|
options.preventCircularRef &&
|
||||||
isVisibleRes &&
|
isVisibleRes &&
|
||||||
prop.kind !== ReferenceKind.SCALAR
|
prop.kind !== ReferenceKind.SCALAR
|
||||||
) {
|
) {
|
||||||
// mapToPk would represent a foreign key and we want to keep them
|
|
||||||
if (!!prop.mapToPk) {
|
if (!!prop.mapToPk) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return !parents.some((parent) => parent === prop.type)
|
const parentsLen = parentsArray.length
|
||||||
|
for (let i = 0; i < parentsLen; i++) {
|
||||||
|
if (parentsArray[i] === prop.type) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return isVisibleRes
|
return isVisibleRes
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EntitySerializer {
|
export class EntitySerializer {
|
||||||
|
// Thread-safe per-instance cache to avoid concurrency issues
|
||||||
|
private static readonly PROPERTY_CACHE_SIZE = 2000
|
||||||
|
|
||||||
static serialize<T extends object, P extends string = never>(
|
static serialize<T extends object, P extends string = never>(
|
||||||
entity: T,
|
entity: T,
|
||||||
options: CustomSerializeOptions<T, P> = {},
|
options: Partial<typeof STATIC_OPTIONS_SHAPE> = STATIC_OPTIONS_SHAPE,
|
||||||
parents: string[] = []
|
parents: string[] = EMPTY_ARRAY
|
||||||
): EntityDTO<Loaded<T, P>> {
|
): EntityDTO<Loaded<T, P>> {
|
||||||
const parents_ = Array.from(new Set(parents))
|
// Avoid Array.from and Set allocation for hot path
|
||||||
|
const parents_ = parents.length > 0 ? Array.from(new Set(parents)) : []
|
||||||
|
|
||||||
const wrapped = helper(entity)
|
const wrapped = helper(entity)
|
||||||
const meta = wrapped.__meta
|
const meta = wrapped.__meta
|
||||||
@@ -141,63 +219,93 @@ export class EntitySerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ret = {} as EntityDTO<Loaded<T, P>>
|
const ret = {} as EntityDTO<Loaded<T, P>>
|
||||||
const keys = new Set<string>(meta.primaryKeys)
|
|
||||||
Object.keys(entity).forEach((prop) => keys.add(prop))
|
// Use Set for deduplication but keep it simple
|
||||||
|
const keys = new Set<string>()
|
||||||
|
|
||||||
|
const primaryKeys = meta.primaryKeys
|
||||||
|
const primaryKeysLen = primaryKeys.length
|
||||||
|
for (let i = 0; i < primaryKeysLen; i++) {
|
||||||
|
keys.add(primaryKeys[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityKeys = Object.keys(entity)
|
||||||
|
const entityKeysLen = entityKeys.length
|
||||||
|
for (let i = 0; i < entityKeysLen; i++) {
|
||||||
|
keys.add(entityKeys[i])
|
||||||
|
}
|
||||||
|
|
||||||
const visited = root.visited.has(entity)
|
const visited = root.visited.has(entity)
|
||||||
if (!visited) {
|
if (!visited) {
|
||||||
root.visited.add(entity)
|
root.visited.add(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
;[...keys]
|
const keysArray = Array.from(keys)
|
||||||
/** Medusa Custom properties filtering **/
|
const keysLen = keysArray.length
|
||||||
.filter((prop) =>
|
|
||||||
filterEntityPropToSerialize({
|
|
||||||
propName: prop,
|
|
||||||
meta,
|
|
||||||
options,
|
|
||||||
parents: parents_,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.map((prop) => {
|
|
||||||
const cycle = root.visit(meta.className, prop)
|
|
||||||
|
|
||||||
if (cycle && visited) {
|
// Hoist invariant calculations
|
||||||
return [prop, undefined]
|
const className = meta.className
|
||||||
|
const platform = wrapped.__platform
|
||||||
|
const skipNull = options.skipNull
|
||||||
|
const metaProperties = meta.properties
|
||||||
|
const preventCircularRef = options.preventCircularRef
|
||||||
|
|
||||||
|
// Clean property processing loop
|
||||||
|
for (let i = 0; i < keysLen; i++) {
|
||||||
|
const prop = keysArray[i]
|
||||||
|
|
||||||
|
// Simple filtering logic
|
||||||
|
const isVisibleRes = isVisible(meta, prop, options)
|
||||||
|
const propMeta = metaProperties[prop]
|
||||||
|
|
||||||
|
let shouldSerialize = isVisibleRes
|
||||||
|
if (
|
||||||
|
propMeta &&
|
||||||
|
preventCircularRef &&
|
||||||
|
isVisibleRes &&
|
||||||
|
propMeta.kind !== ReferenceKind.SCALAR
|
||||||
|
) {
|
||||||
|
if (!!propMeta.mapToPk) {
|
||||||
|
shouldSerialize = true
|
||||||
|
} else {
|
||||||
|
const parentsLen = parents_.length
|
||||||
|
for (let j = 0; j < parentsLen; j++) {
|
||||||
|
if (parents_[j] === propMeta.type) {
|
||||||
|
shouldSerialize = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const val = this.processProperty<T>(
|
if (!shouldSerialize) {
|
||||||
prop as keyof T & string,
|
continue
|
||||||
entity,
|
}
|
||||||
options,
|
|
||||||
parents_
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!cycle) {
|
const cycle = root.visit(className, prop)
|
||||||
root.leave(meta.className, prop)
|
if (cycle && visited) continue
|
||||||
}
|
|
||||||
|
|
||||||
if (options.skipNull && Utils.isPlainObject(val)) {
|
const val = this.processProperty<T>(
|
||||||
Utils.dropUndefinedProperties(val, null)
|
prop as keyof T & string,
|
||||||
}
|
entity,
|
||||||
|
options,
|
||||||
return [prop, val]
|
parents_
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
([, value]) =>
|
|
||||||
typeof value !== "undefined" && !(value === null && options.skipNull)
|
|
||||||
)
|
|
||||||
.forEach(
|
|
||||||
([prop, value]) =>
|
|
||||||
(ret[
|
|
||||||
this.propertyName(
|
|
||||||
meta,
|
|
||||||
prop as keyof T & string,
|
|
||||||
wrapped.__platform
|
|
||||||
)
|
|
||||||
] = value as T[keyof T & string])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!cycle) {
|
||||||
|
root.leave(className, prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipNull && Utils.isPlainObject(val)) {
|
||||||
|
Utils.dropUndefinedProperties(val, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof val !== "undefined" && !(val === null && skipNull)) {
|
||||||
|
ret[this.propertyName(meta, prop as keyof T & string, platform)] =
|
||||||
|
val as T[keyof T & string]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (contextCreated) {
|
if (contextCreated) {
|
||||||
root.close()
|
root.close()
|
||||||
}
|
}
|
||||||
@@ -206,79 +314,118 @@ export class EntitySerializer {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// decorated getters
|
// Clean getter processing
|
||||||
meta.props
|
const metaProps = meta.props
|
||||||
.filter(
|
const metaPropsLen = metaProps.length
|
||||||
(prop) =>
|
|
||||||
prop.getter &&
|
|
||||||
prop.getterName === undefined &&
|
|
||||||
typeof entity[prop.name] !== "undefined" &&
|
|
||||||
isVisible(meta, prop.name, options)
|
|
||||||
)
|
|
||||||
.forEach(
|
|
||||||
(prop) =>
|
|
||||||
(ret[this.propertyName(meta, prop.name, wrapped.__platform)] =
|
|
||||||
this.processProperty(prop.name, entity, options, parents_))
|
|
||||||
)
|
|
||||||
|
|
||||||
// decorated get methods
|
for (let i = 0; i < metaPropsLen; i++) {
|
||||||
meta.props
|
const prop = metaProps[i]
|
||||||
.filter(
|
const propName = prop.name
|
||||||
(prop) =>
|
|
||||||
prop.getterName &&
|
// Clear, readable conditions
|
||||||
(entity[prop.getterName] as unknown) instanceof Function &&
|
if (
|
||||||
isVisible(meta, prop.name, options)
|
prop.getter &&
|
||||||
)
|
prop.getterName === undefined &&
|
||||||
.forEach(
|
typeof entity[propName] !== "undefined" &&
|
||||||
(prop) =>
|
isVisible(meta, propName, options)
|
||||||
(ret[this.propertyName(meta, prop.name, wrapped.__platform)] =
|
) {
|
||||||
this.processProperty(
|
ret[this.propertyName(meta, propName, platform)] = this.processProperty(
|
||||||
prop.getterName as keyof T & string,
|
propName,
|
||||||
entity,
|
entity,
|
||||||
options,
|
options,
|
||||||
parents_
|
parents_
|
||||||
))
|
)
|
||||||
)
|
} else if (
|
||||||
|
prop.getterName &&
|
||||||
|
(entity[prop.getterName] as unknown) instanceof Function &&
|
||||||
|
isVisible(meta, propName, options)
|
||||||
|
) {
|
||||||
|
ret[this.propertyName(meta, propName, platform)] = this.processProperty(
|
||||||
|
prop.getterName as keyof T & string,
|
||||||
|
entity,
|
||||||
|
options,
|
||||||
|
parents_
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread-safe property name resolution with WeakMap for per-entity caching
|
||||||
|
private static propertyNameCache = new WeakMap<
|
||||||
|
EntityMetadata<any>,
|
||||||
|
Map<string, string>
|
||||||
|
>()
|
||||||
|
|
||||||
private static propertyName<T>(
|
private static propertyName<T>(
|
||||||
meta: EntityMetadata<T>,
|
meta: EntityMetadata<T>,
|
||||||
prop: string,
|
prop: string,
|
||||||
platform?: Platform
|
platform?: Platform
|
||||||
): string {
|
): string {
|
||||||
|
// Use WeakMap per metadata to avoid global cache conflicts
|
||||||
|
let entityCache = this.propertyNameCache.get(meta)
|
||||||
|
if (!entityCache) {
|
||||||
|
entityCache = new Map<string, string>()
|
||||||
|
this.propertyNameCache.set(meta, entityCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${prop}:${platform?.constructor.name || "no-platform"}`
|
||||||
|
|
||||||
|
const cached = entityCache.get(cacheKey)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline property resolution for hot path
|
||||||
|
let result: string
|
||||||
|
const property = meta.properties[prop]
|
||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (meta.properties[prop]?.serializedName) {
|
if (property?.serializedName) {
|
||||||
return meta.properties[prop].serializedName as string
|
result = property.serializedName as string
|
||||||
|
} else if (property?.primary && platform) {
|
||||||
|
result = platform.getSerializedPrimaryKeyField(prop) as string
|
||||||
|
} else {
|
||||||
|
result = prop
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta.properties[prop]?.primary && platform) {
|
// Prevent cache from growing too large
|
||||||
return platform.getSerializedPrimaryKeyField(prop) as string
|
if (entityCache.size >= this.PROPERTY_CACHE_SIZE) {
|
||||||
|
entityCache.clear() // Much faster than selective deletion
|
||||||
}
|
}
|
||||||
|
|
||||||
return prop
|
entityCache.set(cacheKey, result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private static processProperty<T extends object>(
|
private static processProperty<T extends object>(
|
||||||
prop: string,
|
prop: string,
|
||||||
entity: T,
|
entity: T,
|
||||||
options: CustomSerializeOptions<T>,
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
parents: string[] = []
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
},
|
||||||
|
parents: string[] = EMPTY_ARRAY
|
||||||
): T[keyof T] | undefined {
|
): T[keyof T] | undefined {
|
||||||
const parents_ = [...parents, entity.constructor.name]
|
// Avoid array allocation when not needed
|
||||||
|
const parents_ =
|
||||||
|
parents.length > 0
|
||||||
|
? [...parents, entity.constructor.name]
|
||||||
|
: [entity.constructor.name]
|
||||||
|
|
||||||
const parts = prop.split(".")
|
// Handle dotted properties efficiently
|
||||||
|
const parts = prop.split(DOT)
|
||||||
prop = parts[0] as string & keyof T
|
prop = parts[0] as string & keyof T
|
||||||
|
|
||||||
const wrapped = helper(entity)
|
const wrapped = helper(entity)
|
||||||
const property = wrapped.__meta.properties[prop]
|
const property = wrapped.__meta.properties[prop]
|
||||||
const serializer = property?.serializer
|
const serializer = property?.serializer
|
||||||
|
const propValue = entity[prop]
|
||||||
|
|
||||||
// getter method
|
// Fast path for function properties
|
||||||
if ((entity[prop] as unknown) instanceof Function) {
|
if ((propValue as unknown) instanceof Function) {
|
||||||
const returnValue = (
|
const returnValue = (propValue as unknown as () => T[keyof T & string])()
|
||||||
entity[prop] as unknown as () => T[keyof T & string]
|
|
||||||
)()
|
|
||||||
if (!options.ignoreSerializers && serializer) {
|
if (!options.ignoreSerializers && serializer) {
|
||||||
return serializer(returnValue)
|
return serializer(returnValue)
|
||||||
}
|
}
|
||||||
@@ -287,10 +434,11 @@ export class EntitySerializer {
|
|||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!options.ignoreSerializers && serializer) {
|
if (!options.ignoreSerializers && serializer) {
|
||||||
return serializer(entity[prop])
|
return serializer(propValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isCollection(entity[prop])) {
|
// Type checks in optimal order
|
||||||
|
if (Utils.isCollection(propValue)) {
|
||||||
return this.processCollection(
|
return this.processCollection(
|
||||||
prop as keyof T & string,
|
prop as keyof T & string,
|
||||||
entity,
|
entity,
|
||||||
@@ -299,7 +447,7 @@ export class EntitySerializer {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isEntity(entity[prop], true)) {
|
if (Utils.isEntity(propValue, true)) {
|
||||||
return this.processEntity(
|
return this.processEntity(
|
||||||
prop as keyof T & string,
|
prop as keyof T & string,
|
||||||
entity,
|
entity,
|
||||||
@@ -311,64 +459,102 @@ export class EntitySerializer {
|
|||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (property?.reference === ReferenceKind.EMBEDDED) {
|
if (property?.reference === ReferenceKind.EMBEDDED) {
|
||||||
if (Array.isArray(entity[prop])) {
|
if (Array.isArray(propValue)) {
|
||||||
return (entity[prop] as object[]).map((item) =>
|
return (propValue as object[]).map((item) =>
|
||||||
helper(item).toJSON()
|
helper(item).toJSON()
|
||||||
) as T[keyof T]
|
) as T[keyof T]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Utils.isObject(entity[prop])) {
|
if (Utils.isObject(propValue)) {
|
||||||
return helper(entity[prop]).toJSON() as T[keyof T]
|
return helper(propValue).toJSON() as T[keyof T]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customType = property?.customType
|
const customType = property?.customType
|
||||||
|
|
||||||
if (customType) {
|
if (customType) {
|
||||||
return customType.toJSON(entity[prop], wrapped.__platform)
|
return customType.toJSON(propValue, wrapped.__platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapped.__platform.normalizePrimaryKey(
|
return wrapped.__platform.normalizePrimaryKey(
|
||||||
entity[prop] as unknown as IPrimaryKey
|
propValue as unknown as IPrimaryKey
|
||||||
) as unknown as T[keyof T]
|
) as unknown as T[keyof T]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static extractChildOptions<T extends object, U extends object>(
|
private static extractChildOptions<T extends object>(
|
||||||
options: CustomSerializeOptions<T>,
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
},
|
||||||
prop: keyof T & string
|
prop: keyof T & string
|
||||||
): CustomSerializeOptions<U> {
|
): Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
} {
|
||||||
|
const propPrefix = prop + DOT
|
||||||
|
const propPrefixLen = propPrefix.length
|
||||||
|
|
||||||
|
// Inline function to avoid call overhead
|
||||||
const extractChildElements = (items: string[]) => {
|
const extractChildElements = (items: string[]) => {
|
||||||
return items
|
const result: string[] = []
|
||||||
.filter((field) => field.startsWith(`${prop}.`))
|
const itemsLen = items.length
|
||||||
.map((field) => field.substring(prop.length + 1))
|
|
||||||
|
// Traditional for loop for better performance
|
||||||
|
for (let i = 0; i < itemsLen; i++) {
|
||||||
|
const field = items[i]
|
||||||
|
if (
|
||||||
|
field.length > propPrefixLen &&
|
||||||
|
field.substring(0, propPrefixLen) === propPrefix
|
||||||
|
) {
|
||||||
|
result.push(field.substring(propPrefixLen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const populate = options.populate
|
||||||
...options,
|
const exclude = options.exclude
|
||||||
|
|
||||||
|
// Avoid object spread when possible
|
||||||
|
const result = {
|
||||||
populate:
|
populate:
|
||||||
Array.isArray(options.populate) && !options.populate.includes("*")
|
Array.isArray(populate) && !populate.includes(WILDCARD)
|
||||||
? extractChildElements(options.populate as unknown as string[])
|
? extractChildElements(populate as unknown as string[])
|
||||||
: options.populate,
|
: populate,
|
||||||
exclude:
|
exclude:
|
||||||
Array.isArray(options.exclude) && !options.exclude.includes("*")
|
Array.isArray(exclude) && !exclude.includes(WILDCARD)
|
||||||
? extractChildElements(options.exclude)
|
? extractChildElements(exclude)
|
||||||
: options.exclude,
|
: exclude,
|
||||||
} as CustomSerializeOptions<U>
|
preventCircularRef: options.preventCircularRef,
|
||||||
|
skipNull: options.skipNull,
|
||||||
|
ignoreSerializers: options.ignoreSerializers,
|
||||||
|
forceObject: options.forceObject,
|
||||||
|
} as Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private static processEntity<T extends object>(
|
private static processEntity<T extends object>(
|
||||||
prop: keyof T & string,
|
prop: keyof T & string,
|
||||||
entity: T,
|
entity: T,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
options: CustomSerializeOptions<T>,
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
parents: string[] = []
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
},
|
||||||
|
parents: string[] = EMPTY_ARRAY
|
||||||
): T[keyof T] | undefined {
|
): T[keyof T] | undefined {
|
||||||
const parents_ = [...parents, entity.constructor.name]
|
const parents_ =
|
||||||
|
parents.length > 0
|
||||||
|
? [...parents, entity.constructor.name]
|
||||||
|
: [entity.constructor.name]
|
||||||
|
|
||||||
const child = Reference.unwrapReference(entity[prop] as T)
|
const child = Reference.unwrapReference(entity[prop] as T)
|
||||||
const wrapped = helper(child)
|
const wrapped = helper(child)
|
||||||
|
// Fixed: was incorrectly calling isPopulated(child, prop, options) instead of isPopulated(entity, prop, options)
|
||||||
const populated =
|
const populated =
|
||||||
isPopulated(child, prop, options) && wrapped.isInitialized()
|
isPopulated(entity, prop, options) && wrapped.isInitialized()
|
||||||
const expand = populated || options.forceObject || !wrapped.__managed
|
const expand = populated || options.forceObject || !wrapped.__managed
|
||||||
|
|
||||||
if (expand) {
|
if (expand) {
|
||||||
@@ -387,67 +573,112 @@ export class EntitySerializer {
|
|||||||
private static processCollection<T extends object>(
|
private static processCollection<T extends object>(
|
||||||
prop: keyof T & string,
|
prop: keyof T & string,
|
||||||
entity: T,
|
entity: T,
|
||||||
options: CustomSerializeOptions<T>,
|
options: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
parents: string[] = []
|
preventCircularRef?: boolean
|
||||||
|
populate?: string[] | boolean
|
||||||
|
},
|
||||||
|
parents: string[] = EMPTY_ARRAY
|
||||||
): T[keyof T] | undefined {
|
): T[keyof T] | undefined {
|
||||||
const parents_ = [...parents, entity.constructor.name]
|
const parents_ =
|
||||||
|
parents.length > 0
|
||||||
|
? [...parents, entity.constructor.name]
|
||||||
|
: [entity.constructor.name]
|
||||||
const col = entity[prop] as unknown as Collection<T>
|
const col = entity[prop] as unknown as Collection<T>
|
||||||
|
|
||||||
if (!col.isInitialized()) {
|
if (!col.isInitialized()) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return col.getItems(false).map((item) => {
|
const items = col.getItems(false)
|
||||||
if (isPopulated(item, prop, options)) {
|
const itemsLen = items.length
|
||||||
return this.serialize(
|
const result = new Array(itemsLen)
|
||||||
item,
|
|
||||||
this.extractChildOptions(options, prop),
|
|
||||||
parents_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return helper(item).getPrimaryKey()
|
const childOptions = this.extractChildOptions(options, prop)
|
||||||
}) as unknown as T[keyof T]
|
|
||||||
|
// Check if the collection property itself should be populated
|
||||||
|
// Fixed: was incorrectly calling isPopulated(item, prop, options) instead of isPopulated(entity, prop, options)
|
||||||
|
const shouldPopulateCollection = isPopulated(entity, prop, options)
|
||||||
|
|
||||||
|
for (let i = 0; i < itemsLen; i++) {
|
||||||
|
const item = items[i]
|
||||||
|
if (shouldPopulateCollection) {
|
||||||
|
result[i] = this.serialize(item, childOptions, parents_)
|
||||||
|
} else {
|
||||||
|
result[i] = helper(item).getPrimaryKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as unknown as T[keyof T]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mikroOrmSerializer = <TOutput extends object>(
|
export const mikroOrmSerializer = <TOutput extends object>(
|
||||||
data: any,
|
data: any,
|
||||||
options?: Parameters<typeof EntitySerializer.serialize>[1] & {
|
options?: Partial<
|
||||||
preventCircularRef?: boolean
|
Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||||
populate?: string[] | boolean
|
preventCircularRef: boolean | undefined
|
||||||
}
|
populate: string[] | boolean | undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
): Promise<TOutput> => {
|
): Promise<TOutput> => {
|
||||||
return new Promise<TOutput>((resolve) => {
|
return new Promise<TOutput>((resolve) => {
|
||||||
options ??= {}
|
// Efficient options handling
|
||||||
|
if (!options) {
|
||||||
|
options = STATIC_OPTIONS_SHAPE
|
||||||
|
} else {
|
||||||
|
// Check if we can use static shape
|
||||||
|
let useStatic = true
|
||||||
|
const optionKeys = Object.keys(options)
|
||||||
|
for (let i = 0; i < optionKeys.length; i++) {
|
||||||
|
const key = optionKeys[i] as keyof typeof options
|
||||||
|
if (
|
||||||
|
options[key] !==
|
||||||
|
STATIC_OPTIONS_SHAPE[key as keyof typeof STATIC_OPTIONS_SHAPE]
|
||||||
|
) {
|
||||||
|
useStatic = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useStatic) {
|
||||||
|
options = STATIC_OPTIONS_SHAPE
|
||||||
|
} else {
|
||||||
|
options = { ...STATIC_OPTIONS_SHAPE, ...options }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const data_ = (Array.isArray(data) ? data : [data]).filter(Boolean)
|
const data_ = (Array.isArray(data) ? data : [data]).filter(Boolean)
|
||||||
|
|
||||||
const forSerialization: unknown[] = []
|
const forSerialization: object[] = []
|
||||||
const notForSerialization: unknown[] = []
|
const notForSerialization: object[] = []
|
||||||
|
|
||||||
data_.forEach((object) => {
|
// Simple classification loop
|
||||||
|
const dataLen = data_.length
|
||||||
|
for (let i = 0; i < dataLen; i++) {
|
||||||
|
const object = data_[i]
|
||||||
if (object.__meta) {
|
if (object.__meta) {
|
||||||
return forSerialization.push(object)
|
forSerialization.push(object)
|
||||||
|
} else {
|
||||||
|
notForSerialization.push(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return notForSerialization.push(object)
|
|
||||||
})
|
|
||||||
|
|
||||||
let result: any = forSerialization.map((entity) =>
|
|
||||||
EntitySerializer.serialize(entity, {
|
|
||||||
forceObject: true,
|
|
||||||
populate: ["*"],
|
|
||||||
|
|
||||||
preventCircularRef: true,
|
|
||||||
...options,
|
|
||||||
} as CustomSerializeOptions<any>)
|
|
||||||
) as TOutput[]
|
|
||||||
|
|
||||||
if (notForSerialization.length) {
|
|
||||||
result = result.concat(notForSerialization)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(Array.isArray(data) ? result : result[0])
|
// Pre-allocate result array
|
||||||
|
const forSerializationLen = forSerialization.length
|
||||||
|
const result: any = new Array(forSerializationLen)
|
||||||
|
|
||||||
|
for (let i = 0; i < forSerializationLen; i++) {
|
||||||
|
result[i] = EntitySerializer.serialize(forSerialization[i], options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple result construction
|
||||||
|
let finalResult: any
|
||||||
|
if (notForSerialization.length > 0) {
|
||||||
|
finalResult = result.concat(notForSerialization)
|
||||||
|
} else {
|
||||||
|
finalResult = result
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(Array.isArray(data) ? finalResult : finalResult[0])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user