-
Notifications
You must be signed in to change notification settings - Fork 395
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for defining initial values for all schema types
Co-authored-by: Bjørge Næss <bjoerge@gmail.com> Co-authored-by: Rex Isaac Raphael <rex.raphael@outlook.com> Co-authored-by: Co Espen Hovlandsdal <espen@hovlandsdal.com>
- Loading branch information
1 parent
0717b58
commit 28593a0
Showing
21 changed files
with
835 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 1 addition & 47 deletions
48
packages/@sanity/initial-value-templates/src/parts/Schema.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,48 @@ | ||
import {isPlainObject} from 'lodash' | ||
import {Schema} from '@sanity/types' | ||
import {Template, TemplateBuilder} from './Template' | ||
import {validateInitialValue} from './validate' | ||
import {validateInitialObjectValue} from './validate' | ||
import deepAssign from './util/deepAssign' | ||
import {resolveInitialValueForType} from './resolveInitialValueForType' | ||
import {resolveValue} from './util/resolveValue' | ||
import {isRecord} from './util/isRecord' | ||
|
||
export function isBuilder(template: Template | TemplateBuilder): template is TemplateBuilder { | ||
return typeof (template as TemplateBuilder).serialize === 'function' | ||
} | ||
|
||
async function resolveInitialValue( | ||
export async function resolveInitialValue( | ||
schema: Schema, | ||
template: Template | TemplateBuilder, | ||
params: {[key: string]: any} = {} | ||
): Promise<{[key: string]: any}> { | ||
// Template builder? | ||
if (isBuilder(template)) { | ||
return resolveInitialValue(template.serialize(), params) | ||
return resolveInitialValue(schema, template.serialize(), params) | ||
} | ||
|
||
const {id, value} = template | ||
const {id, schemaType, value} = template | ||
if (!value) { | ||
throw new Error(`Template "${id}" has invalid "value" property`) | ||
} | ||
|
||
// Static value? | ||
if (isPlainObject(value)) { | ||
return validateInitialValue(value, template) | ||
} | ||
let resolvedValue = await resolveValue(value, params) | ||
|
||
// Not an object, so should be a function | ||
if (typeof value !== 'function') { | ||
if (!isRecord(resolvedValue)) { | ||
throw new Error( | ||
`Template "${id}" has invalid "value" property - must be a plain object or a resolver function` | ||
`Template "${id}" has invalid "value" property - must be a plain object or a resolver function returning a plain object` | ||
) | ||
} | ||
|
||
const resolved = await value(params) | ||
return validateInitialValue(resolved, template) | ||
} | ||
// validate default document initial values | ||
resolvedValue = validateInitialObjectValue(resolvedValue, template) | ||
|
||
export {resolveInitialValue} | ||
// Get deep initial values from schema types (note: the initial value from template overrides the types) | ||
const newValue = deepAssign( | ||
(await resolveInitialValueForType(schema.get(schemaType), params)) || {}, | ||
resolvedValue as Record<string, unknown> | ||
) | ||
|
||
// revalidate and return new initial values | ||
// todo: would be better to do validation as part of type resolution | ||
return validateInitialObjectValue(newValue, template) | ||
} |
94 changes: 94 additions & 0 deletions
94
packages/@sanity/initial-value-templates/src/resolveInitialValueForType.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import {isEmpty, resolveTypeName} from '@sanity/util/content' | ||
|
||
import { | ||
ArraySchemaType, | ||
InitialValueParams, | ||
isArraySchemaType, | ||
isObjectSchemaType, | ||
ObjectSchemaType, | ||
SchemaType, | ||
} from '@sanity/types' | ||
|
||
import {randomKey} from '@sanity/util/paths' | ||
import deepAssign from './util/deepAssign' | ||
import {resolveValue} from './util/resolveValue' | ||
|
||
export function getItemType(arrayType: ArraySchemaType, item: unknown): SchemaType | undefined { | ||
const itemTypeName = resolveTypeName(item) | ||
|
||
return itemTypeName === 'object' && arrayType.of.length === 1 | ||
? arrayType.of[0] | ||
: arrayType.of.find((memberType) => memberType.name === itemTypeName) | ||
} | ||
|
||
const MAX_RECURSION_DEPTH = 10 | ||
|
||
/** | ||
* Resolve initial value for the given schema type (recursively) | ||
* | ||
* @param type {SchemaType} this is the name of the document | ||
* @param params {Record<string, unknown>} params is a sanity context object passed to every initial value function | ||
* @param maxDepth {Record<string, unknown>} maximum recursion depth (default 9) | ||
*/ | ||
export function resolveInitialValueForType( | ||
type: SchemaType, | ||
params: InitialValueParams = {}, | ||
maxDepth = MAX_RECURSION_DEPTH | ||
) { | ||
if (maxDepth <= 0) { | ||
return undefined | ||
} | ||
if (isObjectSchemaType(type)) { | ||
return resolveInitialObjectValue(type, params, maxDepth) | ||
} | ||
if (isArraySchemaType(type)) { | ||
return resolveInitialArrayValue(type, params, maxDepth) | ||
} | ||
return resolveValue(type.initialValue, params) | ||
} | ||
|
||
async function resolveInitialArrayValue(type, params: InitialValueParams, maxDepth: number) { | ||
const initialArray = await resolveValue(type.initialValue) | ||
return Array.isArray(initialArray) | ||
? Promise.all( | ||
initialArray.map(async (initialItem) => { | ||
const itemType = getItemType(type, initialItem)! | ||
return isObjectSchemaType(itemType) | ||
? { | ||
...initialItem, | ||
...(await resolveInitialValueForType(itemType, params, maxDepth - 1)), | ||
_key: randomKey(), | ||
} | ||
: initialItem | ||
}) | ||
) | ||
: undefined | ||
} | ||
async function resolveInitialObjectValue( | ||
type: ObjectSchemaType, | ||
params: InitialValueParams, | ||
maxDepth: number | ||
) { | ||
const initialObject: Record<string, unknown> = { | ||
...((await resolveValue(type.initialValue, params)) || {}), | ||
} | ||
|
||
const fieldValues = {} | ||
await Promise.all( | ||
type.fields.map(async (field) => { | ||
const initialFieldValue = await resolveInitialValueForType(field.type, params, maxDepth - 1) | ||
if (initialFieldValue !== undefined && initialFieldValue !== null) { | ||
fieldValues[field.name] = initialFieldValue | ||
} | ||
}) | ||
) | ||
|
||
const merged = deepAssign(fieldValues, initialObject) | ||
if (isEmpty(merged)) { | ||
return undefined | ||
} | ||
if (type.name !== 'object') { | ||
merged._type = type.name | ||
} | ||
return merged | ||
} |
40 changes: 40 additions & 0 deletions
40
packages/@sanity/initial-value-templates/src/util/deepAssign.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import deepAssign from './deepAssign' | ||
|
||
it('ignores undefined values', () => { | ||
expect(deepAssign({foo: undefined}, {bar: undefined})).toStrictEqual({ | ||
foo: undefined, | ||
bar: undefined, | ||
}) | ||
}) | ||
|
||
it('assigns undefined values from source', () => { | ||
expect(deepAssign({foo: 'bar', bar: 'hello'}, {foo: undefined})).toStrictEqual({ | ||
foo: undefined, | ||
bar: 'hello', | ||
}) | ||
}) | ||
|
||
it('assigns non-undefined values from source', () => { | ||
expect(deepAssign({foo: undefined}, {bar: 'hello'})).toStrictEqual({foo: undefined, bar: 'hello'}) | ||
}) | ||
|
||
it('merges non-undefined values', () => { | ||
expect(deepAssign({foo: 'foo'}, {bar: 'bar'})).toStrictEqual({foo: 'foo', bar: 'bar'}) | ||
}) | ||
|
||
it("doesn't merge arrays", () => { | ||
expect(deepAssign({arr: ['foo']}, {arr: ['bar']})).toStrictEqual({arr: ['bar']}) | ||
}) | ||
|
||
it('merges deep', () => { | ||
expect( | ||
deepAssign({some: {deep: {object: true}}}, {some: {deep: {array: ['foo']}}}) | ||
).toStrictEqual({ | ||
some: { | ||
deep: { | ||
array: ['foo'], | ||
object: true, | ||
}, | ||
}, | ||
}) | ||
}) |
19 changes: 19 additions & 0 deletions
19
packages/@sanity/initial-value-templates/src/util/deepAssign.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import {isRecord} from './isRecord' | ||
|
||
// deep object assign for objects | ||
// note: doesn't mutate target | ||
export default function deepAssign( | ||
target: Record<string, unknown>, | ||
source: Record<string, unknown> | ||
): Record<string, unknown> { | ||
const result = {...target, ...source} | ||
|
||
Object.keys(result).forEach((key) => { | ||
const sourceVal = source[key] | ||
const targetVal = target[key] | ||
if (isRecord(sourceVal) && isRecord(targetVal)) { | ||
result[key] = deepAssign(targetVal, sourceVal) | ||
} | ||
}) | ||
return result | ||
} |
5 changes: 5 additions & 0 deletions
5
packages/@sanity/initial-value-templates/src/util/isRecord.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import {isPlainObject} from 'lodash' | ||
|
||
export function isRecord(value: unknown): value is Record<string, unknown> { | ||
return isPlainObject(value) | ||
} |
11 changes: 11 additions & 0 deletions
11
packages/@sanity/initial-value-templates/src/util/resolveValue.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import {InitialValueParams, InitialValueProperty, InitialValueResolver} from '@sanity/types' | ||
|
||
// returns the "resolved" value from an initial value property (e.g. type.initialValue) | ||
export async function resolveValue<InitialValue>( | ||
initialValueOpt: InitialValueProperty<InitialValue>, | ||
params?: InitialValueParams | ||
): Promise<InitialValue | undefined> { | ||
return typeof initialValueOpt === 'function' | ||
? (initialValueOpt as InitialValueResolver<InitialValue>)(params) | ||
: initialValueOpt | ||
} |
Oops, something went wrong.