Skip to content

Commit

Permalink
Schema parser rewrite (#5178)
Browse files Browse the repository at this point in the history
* Update doc comments.

* Convert grammar into TS types.

* Throw when 'property' is set for non-linkingObjects.

* Verify idempotence (already-normalized input is unchanged).

* Test validation of empty objects.

* Update error messages, var names, minor refactor.

* Refactor validation functions to 'Configuration.ts'.

* Refactor 'NAME' and clearly distinguish btwn realm/object/property schema variables.

* Sanitize top-level object schema fields.

* Minor updates.

* Update error messages.

* Fix bug from renaming property 'constructor' to 'ctor'.

* Refactor and add docs to functions.

* Add only defined props (not 'undefined' due to tests).

* Add minor comments.

* Revert back to original logic in 'from-binding'.

* Refactor 'ensure' to use 'assert'.

* Make minor performance improvements.

* Error on nested collections and optional collections.

* Sanitize user input.

* Refactor to pass both object and property name.

* Error when using shorthand in object notation.

* Update nullability for list, set, dictionary, linkingObjects.

* Refactor tests.

* Remove the need to remove undefined fields.

* Rename field from 'constructor' to 'ctor'.

* Remove helper variable.

* Minor updates.

* Update comments.

* Add normalization tests.

* Rewrite normalization of property schema (1st draft).

* Add temporary pseudo code for property schema normalization.

* Add grammar for property schema.
  • Loading branch information
elle-j committed Dec 28, 2022
1 parent eaa35e5 commit c5799aa
Show file tree
Hide file tree
Showing 12 changed files with 1,800 additions and 217 deletions.
2 changes: 1 addition & 1 deletion packages/realm/src/ClassMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class ClassMap {
const canonicalObjectSchema: CanonicalObjectSchema = canonicalRealmSchema[index];
assert.object(canonicalObjectSchema);
// Create the wrapping class first
const constructor = ClassMap.createClass(objectSchema, canonicalObjectSchema.constructor);
const constructor = ClassMap.createClass(objectSchema, canonicalObjectSchema.ctor);
// Create property getters and setters
const properties = new PropertyMap();
// Setting the helpers on the class
Expand Down
128 changes: 106 additions & 22 deletions packages/realm/src/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import {
DefaultObject,
ObjectSchema,
ObjectSchemaProperty,
Realm,
RealmObject,
RealmObjectConstructor,
Expand Down Expand Up @@ -194,9 +195,24 @@ type BaseConfiguration = {
// disableFormatUpgrade?: boolean;
// };

export function validateConfiguration(arg: unknown): asserts arg is Configuration {
assert.object(arg);
const { path, schema, onMigration, sync } = arg;
const OBJECT_SCHEMA_KEYS = new Set<keyof ObjectSchema>(["name", "primaryKey", "embedded", "asymmetric", "properties"]);

const PROPERTY_SCHEMA_KEYS = new Set<keyof ObjectSchemaProperty>([
"type",
"objectType",
"property",
"default",
"optional",
"indexed",
"mapTo",
]);

/**
* Validate the fields of a user-provided Realm configuration.
*/
export function validateConfiguration(config: unknown): asserts config is Configuration {
assert.object(config);
const { path, schema, onMigration, sync } = config;
if (typeof onMigration !== "undefined") {
assert.function(onMigration, "migration");
}
Expand All @@ -211,39 +227,107 @@ export function validateConfiguration(arg: unknown): asserts arg is Configuratio
}
}

export function validateRealmSchema(schema: unknown): asserts schema is Configuration["schema"][] {
assert.array(schema, "schema");
schema.forEach(validateObjectSchema);
/**
* Validate the data types of the fields of a user-provided realm schema.
*/
export function validateRealmSchema(realmSchema: unknown): asserts realmSchema is Configuration["schema"][] {
assert.array(realmSchema, "the realm schema");
for (const objectSchema of realmSchema) {
validateObjectSchema(objectSchema);
}
// TODO: Assert that backlinks point to object schemas that are actually declared
}

export function validateObjectSchema(arg: unknown): asserts arg is ObjectSchema {
if (typeof arg === "function") {
/**
* Validate the data types of the fields of a user-provided object schema.
*/
export function validateObjectSchema(
objectSchema: unknown,
): asserts objectSchema is RealmObjectConstructor | ObjectSchema {
if (typeof objectSchema === "function") {
// Class based model
const clazz = arg as unknown as DefaultObject;
const clazz = objectSchema as unknown as DefaultObject;
// We assert this later, but want a custom error message
if (!(arg.prototype instanceof RealmObject)) {
if (!(objectSchema.prototype instanceof RealmObject)) {
const schemaName = clazz.schema && (clazz.schema as DefaultObject).name;
if (typeof schemaName === "string" && schemaName != arg.name) {
throw new TypeError(`Class '${arg.name}' (declaring '${schemaName}' schema) must extend Realm.Object`);
if (typeof schemaName === "string" && schemaName !== objectSchema.name) {
throw new TypeError(`Class '${objectSchema.name}' (declaring '${schemaName}' schema) must extend Realm.Object`);
} else {
throw new TypeError(`Class '${arg.name}' must extend Realm.Object`);
throw new TypeError(`Class '${objectSchema.name}' must extend Realm.Object`);
}
}
assert.object(clazz.schema, "schema static");
validateObjectSchema(clazz.schema);
} else {
assert.object(arg, "object schema");
const { name, properties, asymmetric, embedded } = arg;
assert.string(name, "name");
assert.object(properties, "properties");
if (typeof asymmetric !== "undefined") {
assert.boolean(asymmetric);
// Schema is passed as an object
assert.object(objectSchema, "the object schema", false);
const { name: objectName, properties, primaryKey, asymmetric, embedded } = objectSchema;
assert.string(objectName, "the object schema name");
assert.object(properties, `${objectName}.properties`, false);
if (primaryKey !== undefined) {
assert.string(primaryKey, `${objectName}.primaryKey`);
}
if (embedded !== undefined) {
assert.boolean(embedded, `${objectName}.embedded`);
}
if (typeof embedded !== "undefined") {
assert.boolean(embedded);
if (asymmetric !== undefined) {
assert.boolean(asymmetric, `${objectName}.asymmetric`);
}

const invalidKeysUsed = filterInvalidKeys(objectSchema, OBJECT_SCHEMA_KEYS);
assert(
!invalidKeysUsed.length,
`Unexpected field(s) found on the schema for object '${objectName}': '${invalidKeysUsed.join("', '")}'.`,
);

for (const propertyName in properties) {
const propertySchema = properties[propertyName];
const isUsingShorthand = typeof propertySchema === "string";
if (!isUsingShorthand) {
validatePropertySchema(objectName, propertyName, propertySchema);
}
}
assert(!asymmetric || !embedded, `Cannot be both asymmetric and embedded`);
}
}

/**
* Validate the data types of a user-provided property schema that ought to use the
* relaxed object notation.
*/
export function validatePropertySchema(
objectName: string,
propertyName: string,
propertySchema: unknown,
): asserts propertySchema is ObjectSchemaProperty {
const displayedName = `${objectName}.${propertyName}`;
assert.object(propertySchema, displayedName, false);
const { type, objectType, optional, property, indexed, mapTo } = propertySchema;
assert.string(type, `${displayedName}.type`);
if (objectType !== undefined) {
assert.string(objectType, `${displayedName}.objectType`);
}
if (optional !== undefined) {
assert.boolean(optional, `${displayedName}.optional`);
}
if (property !== undefined) {
assert.string(property, `${displayedName}.property`);
}
if (indexed !== undefined) {
assert.boolean(indexed, `${displayedName}.indexed`);
}
if (mapTo !== undefined) {
assert.string(mapTo, `${displayedName}.mapTo`);
}
const invalidKeysUsed = filterInvalidKeys(propertySchema, PROPERTY_SCHEMA_KEYS);
assert(
!invalidKeysUsed.length,
`Unexpected field(s) found on the schema for property '${displayedName}': '${invalidKeysUsed.join("', '")}'.`,
);
}

/**
* Get the keys of an object that are not part of the provided valid keys.
*/
function filterInvalidKeys(object: Record<string, unknown>, validKeys: Set<string>): string[] {
return Object.keys(object).filter((key) => !validKeys.has(key));
}
4 changes: 2 additions & 2 deletions packages/realm/src/Realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export class Realm {
return [name, property.default];
}),
);
return [schema.name, { defaults, constructor: schema.constructor }];
return [schema.name, { defaults, constructor: schema.ctor }];
}),
);
}
Expand Down Expand Up @@ -548,7 +548,7 @@ export class Realm {
for (const objectSchema of schemas) {
const extras = this.schemaExtras[objectSchema.name];
if (extras) {
objectSchema.constructor = extras.constructor;
objectSchema.ctor = extras.constructor;
}
for (const property of Object.values(objectSchema.properties)) {
property.default = extras ? extras.defaults[property.name] : undefined;
Expand Down
9 changes: 5 additions & 4 deletions packages/realm/src/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
import { AssertionError, DefaultObject, Realm, TypeAssertionError, binding } from "./internal";

/**
* Expects the condition to be truly
* @throws {@link Error} If the condition is not truly. Throws either the {@link err} given as param
* @param condition The condition that must be truly to avoid throwing.
* Expects the condition to be truthy
* @throws {@link Error} If the condition is not truthy. Throws either the {@link err} given as param
* @param condition The condition that must be truthy to avoid throwing.
* @param err Optional message or error to throw.
* Or a function producing this, which is useful to avoid computing the error message in case it's not needed.
*/
Expand Down Expand Up @@ -98,8 +98,9 @@ assert.symbol = (value: unknown, name?: string): asserts value is symbol => {
assert.object = <K extends string | number | symbol = string, V = unknown>(
value: unknown,
name?: string,
allowArrays = true,
): asserts value is Record<K, V> => {
if (typeof value !== "object" || value === null) {
if (typeof value !== "object" || value === null || (!allowArrays && Array.isArray(value))) {
throw new TypeAssertionError("an object", value, name);
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/realm/src/schema/from-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function fromBindingObjectSchema({
}: BindingObjectSchema): CanonicalObjectSchema {
const properties = [...computedProperties, ...persistedProperties];
const result: CanonicalObjectSchema = {
constructor: undefined,
ctor: undefined,
name,
properties: Object.fromEntries(
properties.map((property) => [property.publicName || property.name, fromBindingPropertySchema(property)]),
Expand Down

0 comments on commit c5799aa

Please sign in to comment.