Skip to content

ModelScope

Wyatt Greenway edited this page Dec 6, 2022 · 7 revisions

class ModelScope extends QueryEngineBase 📜

ModelScope is the "model level" of a query. It manages things like EXISTS, PROJECT, ORDER, INNER_JOIN, etc...

Being a Proxy, it will "listen" for key access, and lookup fields if there is a key access where the key name isn't found on the class instance itself. In this case, it will check the previous model specified on the top of the internal "operation stack" to see if it owns a field with the name of the missing key. If there is a matching field on the top-most model of the stack, then it will use the field found to push a FieldScope onto the "operation stack", and then return that to the user. Take the following example:

let queryRoot = new QueryEngine({ connection });
let userIDFieldScope = queryRoot.User.id;

When we attempt to access the key 'id' on the User "model scope", we will find that no such key exists on the ModelScope class. Now that no such property is found on the ModelScope class, the ProxyClass will call the method MISSING on the ModelScope, and this MISSING method will check if the current model (User) has a field named 'id'. The ModelScope._getField method finds this field, and returns it. The ModelScope then takes this field instance, and uses it to create and return a FieldScope using QueryEngineBase._newFieldScope. Now that we have a FieldScope, we can continue chaining, now using "field level" operators to act on the field we just looked up.

Notes:

  • ModelScope is a sub-part of the QueryEngine, and so is generally referred to simply as the QueryEngine as a whole. This is also the case for QueryEngineBase, and FieldScope, which also make up the QueryEngine as sub-parts, and so are also often referred to simply as "the query engine".

See also: QueryEngineBase, QueryEngine, FieldScope

method ModelScope::_getField(
    fieldName: string,
): Field
📜

Get the specified field (by name) from the top-most model on the internal "operation stack".

This method is called when a key can not be found on the ModelScope instance. The ProxyClass will call the MISSING method when the key is not found, and that method in turn calls this _getField method to see if the key specified was actually a field name on the most recent model in the "operation stack".

Arguments:

  • fieldName: string

    The field name of the field to fetch from the top-most model on the stack.

Return value: Field

The field found, or undefined if no such field is found.


method ModelScope::AND(
    query?: QueryEngine,
): ModelScope
📜

Logical AND, for ANDing operations together.

This method does not need to be called, but it can be called if desired.

This is a "toggle", so as soon as it is used, it will continue to be "active" for all following operations. In Mythix ORM you don't need to specify AND or OR unless you actually need them. By default AND is enabled for all queries. So for example you can do User.where.id.EQ('test').firstName.EQ('John').lastName.EQ('Smith'), which is exactly the same as User.where.id.EQ('test').AND.firstName.EQ('John').AND.lastName.EQ('Smith').

AND can also be called to group conditions. For example, you could create the following query: User.where.id.EQ('test').AND(User.where.lastName.EQ('John').OR.lastName.EQ('Brown')) to create the following SQL query: WHERE id = 'test' AND (lastName = 'John' OR lastName = 'Brown').

Arguments:

  • query?: QueryEngine

    An optional query, which if provided, will create a "condition group" as a result.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::CROSS_JOIN(): ModelScope 📜

Specify a CROSS JOIN operation for joining tables.

This method does not need to be called.

All Mythix ORM "join" operations should come immediately before a table join. For example: User.where.CROSS_JOIN.id.EQ(Role.where.userID). They are "toggles", and so will remain "on" once used. In short, if you specify a CROSS_JOIN at the very beginning of your query, then ALL table joins in the query will be CROSS JOIN, unless you specify other join types, i.e. User.where.CROSS_JOIN.id.EQ(Role.where.userID).AND.INNER_JOIN.id.EQ(Organization.where.userID).

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::DISTINCT(
    field?: Field | LiteralBase | string | false = Model.getPrimaryKeyField(),
): ModelScope
📜

Apply a DISTINCT clause to the query.

This method does not need to be called, but it can be called if desired.

A field is required for this operation to keep the interface consistent across all database drivers... however, the field specified may not actually be used, depending on database support, or other operations being carried out in the query.

If the underlying database supports it (i.e. PostgreSQL), then this will turn into a DISTINCT ON(field) operation. If the database (or operation being carried out), doesn't support DISTINCT ON, then it will fallback to just a DISTINCT across the entire projection. Any DISTINCT operation applied to the query will always be the very first part of the projection, regardless of whatever else is projected.

This method is optionally callable. If not called, then the primary key of the model specified will be used (if available). If no primary key exists on the specified model, then it will fallback to a raw DISTINCT clause prefixing the projection. For example: User.where.DISTINCT.lastName.EQ('Smith') would be distinct on the primary key of User (which would be id in our example). If instead we call the operator, then we can supply our own field or literal: User.where.DISTINCT('User:firstName').lastName.EQ('Smith').

To turn off any previous DISTINCT applied, pass the argument false to DISTINCT: User.where.DISTINCT.lastName.EQ('Smith').DISTINCT(false). In this example the resulting query would have no DISTINCT clause at all, since it was disabled when DISTINCT(false) was called.

DISTINCT changes the nature of the query, and might change how it is carried out by the underlying database driver. For example, a distinct combined with a count call would modify the count query: await User.where.DISTINCT('User:id').count() would actually turn into the following SQL: SELECT COUNT(DISTINCT "users"."id"). This is just one example however... just know that the underlying database driver might alter the query, or take a completely different path to query if a DISTINCT operation is in-play.

Notes:

  • The support for a DISTINCT clause changes wildly across databases. Some might support ON for a specific column, some may not. Some databases might force a certain ORDER when using DISTINCT, some may not... DISTINCT may be supported in sub-queries, or it may not... or might require the query be written differently. Mythix ORM does its best to make a "standard" out of this very non-standard situation (does any SQL actually follow a standard?), but just be aware that DISTINCT might bite you if you are changing database drivers to a database that behaves differently.

Arguments:

  • field?: Field | LiteralBase | string | false (Default: Model.getPrimaryKeyField())

    The field or literal to be DISTINCT ON (if the database supports it). If false is specified, then any previous DISTINCT operation is cleared.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::EXISTS(
    query: QueryEngine,
): ModelScope
📜

Check if any rows match the query provided.

This is on the ModelScope itself because it is never paired with a field, and is an operator that stands all on its own.

It can be used to check the existence of any value in the database. For example, you might want to query on users, but only if those users have an "admin" role: await User.where.email.EQ('test@example.com').EXISTS(Role.where.name.EQ("admin").userID.EQ(new FieldLiteral('User:id')))

Notes:

  • You can execute a NOT EXISTS operation simply by prefixing EXISTS with a .NOT operation, for example query.NOT.EXISTS(...).

Arguments:

  • query: QueryEngine

    The sub-query to execute to check for existence. Use a FieldLiteral to pair it with the primary query.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::Field(
    fieldName: string,
): FieldScope
📜

Specify a FieldScope directly.

This can be useful if you have a name collision, or if you just want to go the direct route to specify a FieldScope.

This will find the field specified on the most recent (top-most) model on the internal "operation stack". If the field is found, then it will be used to create a new FieldScope, which will then be pushed onto the "operation stack", and returned to the user.

Example:

  • // The two queries below are equivalent. The latter
    // can be a good way to request a field if the field
    // name being specified happens to be a name collision
    // (i.e. has the same name as a QueryEngine or ModelScope
    // property... such as "ORDER" or "OFFSET" for example).
    let query1 = User.where.id.EQ('test');
    let query2 = User.where.Field('id').EQ('test');

Notes:

  • An exception will be thrown in the specified field can not be found.
  • This is one of the rare places in Mythix ORM where a fully qualified field name SHOULD NOT be used. The reason should be clear: The model in the operation should already be known.

Arguments:

  • fieldName: string

    A field name (NOT fully qualified), as a string. It must be a field that exists on the model from the top-most ModelScope on the internal "operation stack".

Return value: FieldScope

A new FieldScope, targeted to the field specified by fieldName.


method ModelScope::FULL_JOIN(
    outerJoin?: boolean = false,
): ModelScope
📜

Specify a FULL JOIN operation for joining tables.

This method does not need to be called, but it can be called if desired.

All Mythix ORM "join" operations should come immediately before a table join. For example: User.where.FULL_JOIN.id.EQ(Role.where.userID). They are "toggles", and so will remain "on" once used. In short, if you specify a FULL_JOIN at the very beginning of your query, then ALL table joins in the query will be FULL JOIN, unless you specify other join types, i.e. User.where.FULL_JOIN.id.EQ(Role.where.userID).AND.INNER_JOIN.id.EQ(Organization.where.userID).

Arguments:

  • outerJoin?: boolean (Default: false)

    If true, then this will result in a FULL OUTER JOIN if the database supports it.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::GROUP_BY(
    ...args: Array<Field | LiteralBase | string | '+' | '-' | '*'>,
): ModelScope
📜

Apply a GROUP BY clause to the query.

See ModelScope.mergeFields to better understand how this method works.

Notes:

  • This method will flatten all provided arguments into a one dimensional array, so you can provide arrays or deeply nested arrays for the fields specified.

Arguments:

  • ...args: Array<Field | LiteralBase | string | '+' | '-' | '*'>

    New "fields" to supply to GROUP BY, replacing, adding, or subtracting the specified fields from the GROUP_BY operation.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::HAVING(
    query: QueryEngine,
): ModelScope
📜

Apply a HAVING clause to the query.

This will only be applied in the underlying database query if it is also paired with a ModelScope.GROUP_BY operation, otherwise it will be ignored.

Example:

  • let adultCountByAge = await User.where
      .GROUP_BY('User:age')
      .HAVING(User.where.age.GTE(18))
      .PROJECT('User:age', new CountLiteral('User:age', { as: 'count' }))
      .all();

Arguments:

  • query: QueryEngine

    The query to use to apply conditions to the HAVING clause.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::INNER_JOIN(): ModelScope 📜

Specify an INNER JOIN operation for joining tables.

This method does not need to be called, but it can be called if desired.

All Mythix ORM "join" operations should come immediately before a table join. For example: User.where.INNER_JOIN.id.EQ(Role.where.userID). They are "toggles", and so will remain "on" once used. In short, if you specify an INNER_JOIN at the very beginning of your query, then ALL table joins in the query will be INNER JOIN, unless you specify other join types.

Notes:

  • INNER_JOIN is the default table join type in Mythix ORM if no other table join type is specified in the query.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::JOIN(
    type: Literal | string,
): ModelScope
📜

Specify a custom join operation for the underlying database. It is recommended that you use a literal, though that isn't required.

All Mythix ORM "join" operations should come immediately before a table join. For example: User.where.JOIN('RIGHT INNER JOIN').id.EQ(Role.where.userID). They are "toggles", and so will remain "on" once used.

Notes:

  • This method should be avoided if at all possible, since it will likely be database specific, making your code not as portable to another database driver.
  • Is there a standard join type that Mythix ORM missed, that should be supported across most or all databases? If so, let us know by opening an Issue or PR on our GitHub page. Thank you!

Arguments:

  • type: Literal | string

    The join type to use in the underlying database (a literal value).

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::LEFT_JOIN(
    outerJoin?: boolean = false,
): ModelScope
📜

Specify a LEFT JOIN operation for joining tables.

This method does not need to be called, but it can be called if desired.

All Mythix ORM "join" operations should come immediately before a table join. For example: User.where.LEFT_JOIN.id.EQ(Role.where.userID). They are "toggles", and so will remain "on" once used. In short, if you specify a LEFT_JOIN at the very beginning of your query, then ALL table joins in the query will be LEFT JOIN, unless you specify other join types, i.e. User.where.LEFT_JOIN.id.EQ(Role.where.userID).AND.INNER_JOIN.id.EQ(Organization.where.userID).

Arguments:

  • outerJoin?: boolean (Default: false)

    If true, then this will result in a LEFT OUTER JOIN if the database supports it.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::LIMIT(
    limit: number,
): ModelScope
📜

Apply a LIMIT clause to the query.

Any valid positive integer is acceptable, as well as Infinity. If Infinity is used, then the LIMIT will either turn into its max possible value (in the billions... depending on the underlying database), or it will be stripped from the query entirely. NaN, or anything that isn't a valid positive integer will throw an error.

Notes:

  • Positive floating point numbers are rounded with Math.round.

Arguments:

  • limit: number

    The limit to apply to the query.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::mergeFields(
    currentFields: Map<string, { value: Field | Literal | string; direction?: '+' | '-'; ... }>,
    incomingFields: Array<Model | Field | string | literal | '+' | '-' | '*'>,
    extraData?: object,
    options?: object,
): Map<string, { value: Field | Literal | string; direction?: '+' | '-'; ... }>
📜

Merge fields together, replacing, adding, or subtracting from the field set.

This method is used by ModelScope.ORDER, ModelScope.GROUP_BY, and ModelScope.PROJECT. It works be replacing, adding, or subtracting from the current set of fields defined by any of these operations.

If the first field encountered in the provided incomingFields isn't prefixed with a + or -, and if a + or - operation doesn't come before the encountered field, then the operation is considered a "replace" operation. For example, in query.ORDER('User:id') we are "replacing" the order clause with only a single field: 'User:id'. If however, we did query.ORDER('+User:id') then the 'User:id' field would be added to any current order clause in the query. We could also prefix the entire operation with a single '+' string, and then all following fields in the list would be in "add" mode. For example, the following two operations are equivalent: query.ORDER('+', 'User:firstName', 'User:lastName'), and query.ORDER('+User:firstName', '+User:lastName'). The subtraction operator '-' works the same, but in reverse, specifying that we want to remove fields instead of add them: query.ORDER('-', 'User:firstName', 'User:lastName'), and query.ORDER('-User:firstName', '-User:lastName'). It is also possible to mix the two together: query.ORDER('+', 'User:firstName', '-', 'User:lastName'), or query.ORDER('+User:firstName', '-User:lastName').

There is one special operator, '*'. This operator will add all fields from all models that are currently in use on the query. For example, if we did: Role.where.userID(User.where.id).PROJECT('*'), this would add all Role and User fields to the projection. Replace, add, and subtract rules still apply with a wildcard. So in our example above we are "replacing" the projection, since no + or - was encountered before the operation. If you instead wanted to add all model fields to the projection, you should instead do: Role.where.userID(User.where.id).PROJECT('+*') or Role.where.userID(User.where.id).PROJECT('+', '*'). Generally, it probably wouldn't matter much, since you are replacing "every field" anyhow... but it could matter if for example you had previously projected a literal... in which case it would be replaced with all model fields. Subtraction rules also apply to wildcard selectors.

The only truly important thing to remember here is that if no operation is specified (add or subtract), then the default operation is "replace", so the field set will be replaced entirely for the operation in question.

There is one exception to this behavior, for ORDER.ADD and ORDER.REPLACE only. These two operations allow the field sort direction itself to be defined with + and -... so a query.ORDER.ADD('+User:firstName', '-User:lastName') is not requesting that we add the 'User:firstName' field, and subtract the 'User:lastName' field... but instead is specifying that we want 'User:firstName' in ASC sort order, whereas we want 'User:lastName' in DESC sort order. ORDER.ADD and ORDER.REPLACE are the only exceptions to the normal logic defined here. There are two methods so that a user can add to the field set, or replace the field set. A SUB (subtract) operation is not needed, because that can be done with ORDER anyway, i.e. query.ORDER.ADD('+User:firstName', '+User:lastName').ORDER('-Role'), which would "add" the 'User:firstName' and 'User:lastName' fields, simultaneously specifying their short direction, and then ORDER('-Role') is subtracting every field from the Role model.

Notes:

  • A solo + or - operation is a no-op: query.ORDER('+') and query.ORDER('-') will do nothing at all, and will not modify the operation in the slightest.
  • This method supports a short-cut for literals. You can specify a string prefixed with an @ symbol to specify a literal. For example, query.PROJECT('@COUNT("users"."firstName") AS "count"'). Doing so will create a new Literal instance with the value provided. This is not the best way to provide a literal however, because the Literal class defines a "raw" literal, whereas the typed literal classes (such as CountLiteral) actually store the field they are operating on, and can report that field back to the engine.

Arguments:

  • currentFields: Map<string, { value: Field | Literal | string; direction?: '+' | '-'; ... }>

    A map of the current fields for the operation. This is the value from the operation context itself. For example, the ORDER method provides this argument via: this.getOperationContext().order, providing any previous "order" operation as the current fields.

  • incomingFields: Array<Model | Field | string | literal | '+' | '-' | '*'>

    Incoming fields to replace, add, or subtract from the operation in question. If no fields at all are provided, then this will reset/nullify the operation. For example, an ORDER() would clear any ORDER BY clause entirely. If a model is provided, either as a name, i.e. 'User', or as the raw model, i.e. User, then all the fields from the specified model will be added or subtracted. A field can be specified as a fully qualified field name, a raw Field instance, or just the field name itself, i.e. 'firstName'. If no model is specified (i.e. a non-fully-qualified field name), then the engine will attempt to fetch the field specified from the root model of the query. Literals can be used for all supported operations, ORDER, GROUP_BY, and PROJECT, and they also follow the replace, add and subtract rules of the engine.

  • extraData?: object

    Extra data to apply to the operation. For example, the ORDER operation applies a direction property to each field in the map. The field map is a Map instance, where each key is the fully qualified field name, or expanded literal value. Each value on the map is an object, containing at least a value property that is the field or literal specified. This object can also contain any ancillary operation info, such as in the case of the ORDER operation, which will also add a direction property to each field in the map to specify the field's (or literal's) sort order.

  • options?: object

    An options object to pass off to Literal.toString when expanding literals for use as Map keys.

Return value: Map<string, { value: Field | Literal | string; direction?: '+' | '-'; ... }>

Return the new field set. A Map will always be returned, but it is possible for the Map to be empty.


method ModelScope::NOT(): ModelScope 📜

Invert the logic of the following operator.

This method does not need to be called.

Unlike AND and OR operators, this is not a permanent toggle. As soon as a following operator is encountered, the NOT operation will be "turned off". NOT will invert any operation, so for example if you did a User.where.id.NOT.EQ('test') then this is the same as User.where.id.NEQ('test')--selecting everything NOT EQUAL TO 'test'. NOT can also be used in combination with EXISTS, ANY, ALL, IN, etc... inverting any operator following NOT.

Notes:

  • NOT is only ever used once, and then it is toggled back off. For example, if we did: User.where.id.NOT.EQ('test').AND.lastName.EQ('Smith'), then we would have a query where id != 'test' AND lastName = 'Smith'. As you can see, the NOT doesn't apply to the second lastName operator, as it was turned off as soon as the first EQ operator was encountered on the id field.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::OFFSET(
    offset: number,
): ModelScope
📜

Apply an OFFSET clause to the query.

Any valid positive integer is acceptable. Infinity, NaN, or anything that isn't a valid positive integer will throw an error.

Notes:

  • Positive floating point numbers are rounded with Math.round.

Arguments:

  • offset: number

    The offset to apply to the query.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::OR(
    query?: QueryEngine,
): ModelScope
📜

Logical OR, for ORing operations together.

This method does not need to be called, but it can be called if desired.

This is a "toggle", so as soon as it is used, it will continue to be "active" for all following operations. In Mythix ORM you don't need to specify AND or OR unless you actually need them. By default AND is enabled for all queries. For example you can do User.where.id.EQ('test').OR.firstName.EQ('John').lastName.EQ('Smith'), which is exactly the same as User.where.id.EQ('test').OR.firstName.EQ('John').OR.lastName.EQ('Smith').

OR can also be called to group conditions. For example, you could create the following query: User.where.id.EQ('test').OR(User.where.firstName.EQ('John').OR.lastName.EQ('Brown')) to create the following SQL query: WHERE id = 'test' OR (firstName = 'John' AND lastName = 'Brown').

Arguments:

  • query?: QueryEngine

    An optional query, which if provided, will create a "condition group" as a result.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::ORDER(
    ...args: Array<Field | LiteralBase | string | '+' | '-' | '*'>,
): ModelScope
📜

Apply an ORDER BY clause to the query, to sort the results on the fields specified, in either ASC or DESC order.

There are five variants to this method, ORDER.ASC, ORDER.DESC, ORDER.ADD, ORDER.REPLACE, and ORDER (which is an alias for ORDER.ASC), .

  1. ORDER - Alias for ORDER.ASC.
  2. ORDER.ASC - Follow the rules of ModelScope.mergeFields. Each field/literal added is in ASC order.
  3. ORDER.DESC - Follow the rules of ModelScope.mergeFields. Each field/literal added is in DESC order.
  4. ORDER.ADD - DO NOT follow the rules of ModelScope.mergeFields, and instead add all fields specified, with their sort order being specified instead by the + or - prefixes on each field.
  5. ORDER.REPLACE - DO NOT follow the rules of ModelScope.mergeFields, and instead replace the operation fields to the fields specified, with their sort order being specified instead by the + or - prefixes on each field.

See ModelScope.mergeFields to better understand how this method works.

Notes:

  • This method will flatten all provided arguments into a one dimensional array, so you can provide arrays or deeply nested arrays for the fields specified.

Arguments:

  • ...args: Array<Field | LiteralBase | string | '+' | '-' | '*'>

    New "fields" to supply to ORDER, replacing, adding, or subtracting the specified fields from the ORDER operation.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::PROJECT(
    ...args: Array<Field | LiteralBase | string | '+' | '-' | '*'>,
): ModelScope
📜

Replace, add to, or subtract from the projection of the query.

See ModelScope.mergeFields to better understand how this method works.

Notes:

  • This method will flatten all provided arguments into a one dimensional array, so you can provide arrays or deeply nested arrays for the fields specified.
  • Mythix ORM collects and returns models (or partial models) based on the projection. By default only the "root model" of a query will be projected and converted into model instances. If you want to fetch more than just the root model while querying, make sure to project the models (or fields from other models) that you want to collect into model instances during load.

Arguments:

  • ...args: Array<Field | LiteralBase | string | '+' | '-' | '*'>

    New "fields" to supply to the projection, replacing, adding, or subtracting the specified fields from the PROJECT operation.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.


method ModelScope::RIGHT_JOIN(
    outerJoin?: boolean = false,
): ModelScope
📜

Specify a RIGHT JOIN operation for joining tables.

This method does not need to be called, but it can be called if desired.

All Mythix ORM "join" operations should come immediately before a table join. For example: User.where.RIGHT_JOIN.id.EQ(Role.where.userID). They are "toggles", and so will remain "on" once used. In short, if you specify a RIGHT_JOIN at the very beginning of your query, then ALL table joins in the query will be RIGHT JOIN, unless you specify other join types, i.e. User.where.RIGHT_JOIN.id.EQ(Role.where.userID).AND.INNER_JOIN.id.EQ(Organization.where.userID).

Arguments:

  • outerJoin?: boolean (Default: false)

    If true, then this will result in a RIGHT OUTER JOIN if the database supports it.

Return value: ModelScope

Return a ModelScope to allow the user to continue chaining operations on the query.



Clone this wiki locally