jsonschema-definer icon indicating copy to clipboard operation
jsonschema-definer copied to clipboard

Feature: Omit, Pick, and Intersection utility

Open TriStarGod opened this issue 3 years ago • 4 comments

Would be great if there was an omit or pick option for the Object type. Something like

const t1 = s.shape({ id: s.string(), text: s.string(), text2: s.string() })
const t2 = t1.omit("text", "text2");

TriStarGod avatar May 24 '21 16:05 TriStarGod

Added the following to your object.ts file. Not sure how to get the types outputted correctly.

	/**
	 * Return new ObjectSchema with omitted fields
	 *
	 * @returns {ObjectSchema}
	 */
	omit<K extends keyof T>(...keys: K[]) {
		const es = new Set(keys as string[]);
		const { properties, required, ...plain } = this.valueOf();
		const np = {} as ObjectJsonSchema;
		for (const key in properties) {
			if (!es.has(key)) np[key] = properties[key];
		}
		(plain.properties as Record<string, unknown>) = np;
		if (required) {
			const nrs = [];
			for (let i = 0; i < required.length; i++) {
				const r = required[i];
				if (!es.has(r as any)) nrs.push(r);
			}
			if (nrs.length > 0) (plain.required as string[]) = nrs;
		}
		return Object.assign(Object.create(this.constructor.prototype), {
			...this,
			plain,
		});
	}

	/**
	 * Return new ObjectSchema with picked fields
	 *
	 * @returns {ObjectSchema}
	 */
	pick<K extends keyof T>(...keys: K[]) {
		// const plain = {} as Record<string, unknown>;
		const { properties = {}, required, ...plain } = this.valueOf();
		for (const key of keys) {
			(plain[key as string] as Record<string, unknown>) = properties[
				key as string
			];
		}
		if (required) {
			const nrs = [];
			const ins = new Set(keys as string[]);
			for (let i = 0; i < required.length; i++) {
				const r = required[i];
				if (ins.has(r)) nrs.push(r);
			}
			if (nrs.length > 0) (plain.required as string[]) = nrs;
		}
		return Object.assign(Object.create(this.constructor.prototype), {
			...this,
			plain,
		});
	}

I noticed your ajv and ts-toolbelt are old. Probably should upgrade. Also, your ajv addKeyword's validate function is failing typescript.

TriStarGod avatar May 25 '21 05:05 TriStarGod

Added omit and pick functions with correct type outputs. Also fixed partial's output since it wasn't outputting correctly for me.

TriStarGod avatar May 26 '21 21:05 TriStarGod

Added an "intersection" object feature. Identical to the shape found in the index but with some typescript adjustments. Fixed a couple of errors. Having some difficultly with "intersection" type output. Typescript is tough... Can't edit the pull request :(

/**
 * Return new ObjectSchema with picked fields
 *
 * @returns {ObjectSchema}
 */
pick<K extends keyof T>(...keys: K[]): ObjectSchema<Pick<T, K>, R> {
	const plain = { ...this.valueOf() };
	const { properties, required } = plain;
	const nps = {} as ObjectJsonSchema;
	if (properties) {
		for (const key of keys) {
			nps[key as keyof ObjectJsonSchema] =
				properties[key as keyof ObjectJsonSchema];
		}
		plain.properties = nps;
	}
	if (required) {
		const nrs = [];
		const ins = new Set(keys as string[]);
		for (let i = 0; i < required.length; i++) {
			const r = required[i];
			if (ins.has(r)) nrs.push(r);
		}
		if (nrs.length > 0) (plain.required as string[]) = nrs;
	}
	return Object.assign(Object.create(this.constructor.prototype), {
		...this,
		plain,
	});
}

/**
 * Intersections ObjectSchemas
 * Return new ObjectSchema with combined fields.
 * The new object take precedence for similar properties.
 *
 * @returns {ObjectSchema}
 */
intersection<A extends Record<string, any>>(
	props: ObjectSchema<A, boolean>,
	additional = false,
): ObjectSchema<A & T, boolean> {
	const res = new ObjectSchema<A & T>()
		.additionalProperties(additional)
		.copyWith(this as ObjectSchema);
	const required = [...(props.plain.required || [])];
	for (const prop of this.plain.required || []) {
		if (!required.includes(prop)) required.push(prop);
	}
	return res.copyWith({
		plain: {
			properties: {
				...this.plain.properties,
				...props.plain.properties,
			},
			...(required.length > 0 && {
				required,
			}),
		},
	});
}

/**
 * Intersections ObjectSchema with Object (like shape)
 * Return new ObjectSchema with combined fields.
 * The new props take precedence for similar properties.
 *
 * @returns {ObjectSchema}
 */
intersectionShape<X extends Record<string, BaseSchema<any, boolean>>>(
	props: X,
	additional = false,
) {
	let res = new ObjectSchema<
		Optional<{ [Y in keyof X]: X[Y]["otype"] }> & T
	>()
		.additionalProperties(additional)
		.copyWith(this as ObjectSchema);
	for (const name in props) {
		const schema = props[name];
		const { required = [] } = this.plain;
		res = res.copyWith({
			plain: {
				properties: {
					...this.plain.properties,
					[name]: schema.plain,
				},
				...(schema.isRequired &&
					!required.includes(name) && {
						required: [...required, name],
					}),
			},
		});
	}
	return res;
}

TriStarGod avatar May 27 '21 14:05 TriStarGod

I made many more improvements and since this OP seems to be AFK, created my own repo - https://github.com/TakitoTech/schemez

TriStarGod avatar Jul 07 '22 07:07 TriStarGod