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(sanity): memoize initial value resolver #6614

Merged
merged 3 commits into from
May 16, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export function getTemplatePermissions({
template,
item.parameters,
context,
{
useCache: true,
binoy14 marked this conversation as resolved.
Show resolved Hide resolved
},
)

return {template, item, resolvedInitialValue}
Expand Down
91 changes: 90 additions & 1 deletion packages/sanity/src/core/templates/__tests__/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import {Schema as SchemaBuilder} from '@sanity/schema'
import {type InitialValueResolverContext} from '@sanity/types'
import {omit} from 'lodash'

import {resolveInitialValue, type Template} from '../'
import {type resolveInitialValue as resolveInitialValueType, type Template} from '../'
import {schema} from './schema'

let resolveInitialValue: typeof resolveInitialValueType

beforeEach(() => {
jest.resetModules()
jest.clearAllMocks()

resolveInitialValue = require('../').resolveInitialValue
})

const example: Template = {
Expand Down Expand Up @@ -333,4 +338,88 @@ describe('resolveInitialValue', () => {
})
})
})

describe('memoizes function calls', () => {
const initialValue = jest.fn().mockReturnValue('Name')

const testSchema = SchemaBuilder.compile({
name: 'default',
types: [
{
name: 'author',
title: 'Author',
type: 'document',
fields: [
{
name: 'name',
type: 'string',
initialValue,
},
],
},
],
})

test('memoizes function calls', async () => {
for (let index = 0; index < 2; index++) {
await resolveInitialValue(testSchema, example, {}, mockConfigContext, {useCache: true})
}

expect(initialValue).toHaveBeenCalledTimes(1)
})

test('calls function again if params change', async () => {
for (let index = 0; index < 2; index++) {
await resolveInitialValue(testSchema, example, {index}, mockConfigContext, {useCache: true})
}

expect(initialValue).toHaveBeenCalledTimes(2)
})

test('calls function again if context.projectId changes', async () => {
for (let index = 0; index < 2; index++) {
await resolveInitialValue(
testSchema,
example,
{},
{projectId: index.toString()} as InitialValueResolverContext,
{useCache: true},
)
}

expect(initialValue).toHaveBeenCalledTimes(2)
})

test('calls function again if context.dataset changes', async () => {
for (let index = 0; index < 2; index++) {
await resolveInitialValue(
testSchema,
example,
{},
{dataset: index.toString()} as InitialValueResolverContext,
{useCache: true},
)
}

expect(initialValue).toHaveBeenCalledTimes(2)
})

test('calls function again if context.currentUser.id changes', async () => {
for (let index = 0; index < 2; index++) {
await resolveInitialValue(
testSchema,
example,
{},
{
currentUser: {
id: index.toString(),
},
} as InitialValueResolverContext,
{useCache: true},
)
}

expect(initialValue).toHaveBeenCalledTimes(2)
})
})
})
72 changes: 59 additions & 13 deletions packages/sanity/src/core/templates/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,58 @@ export type Serializeable<T> = {
serialize(): T
}

interface Options {
useCache?: boolean
}

/** @internal */
export function isBuilder(template: unknown): template is Serializeable<Template> {
return isRecord(template) && typeof template.serialize === 'function'
}

const cache = new WeakMap<
InitialValueResolver<unknown, unknown>,
Record<string, unknown | Promise<unknown>>
>()

/** @internal */
// returns the "resolved" value from an initial value property (e.g. type.initialValue)
// eslint-disable-next-line require-await
export async function resolveValue<Params, InitialValue>(
initialValueOpt: InitialValueProperty<Params, InitialValue>,
params: Params | undefined,
context: InitialValueResolverContext,
options?: Options,
): Promise<InitialValue | undefined> {
return typeof initialValueOpt === 'function'
? (initialValueOpt as InitialValueResolver<Params, InitialValue>)(params, context)
: initialValueOpt
const useCache = options?.useCache

if (typeof initialValueOpt === 'function') {
const cached = cache.get(initialValueOpt as InitialValueResolver<unknown, unknown>)

const key = JSON.stringify([
params,
context.projectId,
context.dataset,
context.currentUser?.id,
])

if (useCache && cached?.[key]) {
return cached[key] as InitialValue | Promise<InitialValue>
}

const value = (initialValueOpt as InitialValueResolver<Params, InitialValue>)(params, context)

if (useCache) {
cache.set(initialValueOpt as InitialValueResolver<unknown, unknown>, {
...cached,
[key]: value,
})
}

return value
}

return initialValueOpt
}

/** @internal */
Expand All @@ -45,18 +81,19 @@ export async function resolveInitialValue(
template: Template,
params: {[key: string]: any} = {},
context: InitialValueResolverContext,
options?: Options,
): Promise<{[key: string]: any}> {
// Template builder?
if (isBuilder(template)) {
return resolveInitialValue(schema, template.serialize(), params, context)
return resolveInitialValue(schema, template.serialize(), params, context, options)
}

const {id, schemaType: schemaTypeName, value} = template
if (!value) {
throw new Error(`Template "${id}" has invalid "value" property`)
}

let resolvedValue = await resolveValue(value, params, context)
let resolvedValue = await resolveValue(value, params, context, options)

if (!isRecord(resolvedValue)) {
throw new Error(
Expand All @@ -79,8 +116,13 @@ export async function resolveInitialValue(
}

const newValue = deepAssign(
(await resolveInitialValueForType(schemaType, params, DEFAULT_MAX_RECURSION_DEPTH, context)) ||
{},
(await resolveInitialValueForType(
schemaType,
params,
DEFAULT_MAX_RECURSION_DEPTH,
context,
options,
)) || {},
resolvedValue as Record<string, unknown>,
)

Expand Down Expand Up @@ -120,29 +162,31 @@ export function resolveInitialValueForType<Params extends Record<string, unknown
*/
maxDepth = DEFAULT_MAX_RECURSION_DEPTH,
context: InitialValueResolverContext,
options?: Options,
): Promise<any> {
if (maxDepth <= 0) {
return Promise.resolve(undefined)
}

if (isObjectSchemaType(type)) {
return resolveInitialObjectValue(type, params, maxDepth, context)
return resolveInitialObjectValue(type, params, maxDepth, context, options)
}

if (isArraySchemaType(type)) {
return resolveInitialArrayValue(type, params, maxDepth, context)
return resolveInitialArrayValue(type, params, maxDepth, context, options)
}

return resolveValue(type.initialValue, params, context)
return resolveValue(type.initialValue, params, context, options)
}

async function resolveInitialArrayValue<Params extends Record<string, unknown>>(
type: SchemaType,
params: Params,
maxDepth: number,
context: InitialValueResolverContext,
options?: Options,
): Promise<any> {
const initialArray = await resolveValue(type.initialValue, undefined, context)
const initialArray = await resolveValue(type.initialValue, undefined, context, options)

if (!Array.isArray(initialArray)) {
return undefined
Expand All @@ -154,7 +198,7 @@ async function resolveInitialArrayValue<Params extends Record<string, unknown>>(
return isObjectSchemaType(itemType)
? {
...initialItem,
...(await resolveInitialValueForType(itemType, params, maxDepth - 1, context)),
...(await resolveInitialValueForType(itemType, params, maxDepth - 1, context, options)),
_key: randomKey(),
}
: initialItem
Expand All @@ -168,9 +212,10 @@ export async function resolveInitialObjectValue<Params extends Record<string, un
params: Params,
maxDepth: number,
context: InitialValueResolverContext,
options?: Options,
): Promise<any> {
const initialObject: Record<string, unknown> = {
...((await resolveValue(type.initialValue, params, context)) || {}),
...((await resolveValue(type.initialValue, params, context, options)) || {}),
}

const fieldValues: Record<string, any> = {}
Expand All @@ -181,6 +226,7 @@ export async function resolveInitialObjectValue<Params extends Record<string, un
params,
maxDepth - 1,
context,
options,
)
if (initialFieldValue !== undefined && initialFieldValue !== null) {
fieldValues[field.name] = initialFieldValue
Expand Down