node-cosmos
Version:
A light weight azure cosmosdb client aiming at ease of use for creating REST API. Supports json filter, sort and offset/limit
191 lines (161 loc) • 4.93 kB
text/typescript
import { SqlQuerySpec } from "@azure/cosmos";
import { assertNotEmpty } from "../../util/assert";
import { parse } from "./Expression";
// A type for json
export type Json = null | boolean | number | string | JsonArray | JsonObject;
export type JsonArray = Array<Json>;
export interface JsonObject {
[key: string]: Json;
}
/**
* Condition for find. (e.g. filter / sort / offset / limit / fields)
*/
export interface Condition {
filter?: JsonObject;
sort?: [string, string];
offset?: number;
limit?: number;
fields?: string[];
}
/**
* Default find limit to protect db. override this by setting condition.limit explicitly.
*/
export const DEFAULT_LIMIT = 100;
/**
* User defined type guard for JsonObject
* @param json
*/
export const isJsonObject = (json: Json | undefined): json is JsonObject => {
return (
json !== undefined &&
json !== null &&
typeof json !== "boolean" &&
typeof json !== "number" &&
typeof json !== "string" &&
!Array.isArray(json)
);
};
/**
* convert condition to a querySpec (SQL and params)
* @param condition
*/
export const toQuerySpec = (condition: Condition, countOnly?: boolean): SqlQuerySpec => {
const { filter: _filter, sort = [], offset } = condition;
let { limit } = condition;
//TODO fields
const fields = countOnly ? "COUNT(1)" : "*";
// filters
const { queries, params } = _generateFilter(_filter);
let queryText = [`SELECT ${fields} FROM root r`, queries.join(" AND ")]
.filter((s) => s)
.join(" WHERE ");
// sort
if (!countOnly && sort) {
// r.name
const order = sort.length ? " ORDER BY " + _formatKey(sort[0]) : "";
// ASC
const order2 = sort.length > 1 ? ` ${sort[1]}` : "";
queryText += order + order2;
}
// offset and limit
if (!countOnly) {
//default limit is 100 to protect db
limit = limit || DEFAULT_LIMIT;
// https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-sql-query#OffsetLimitClause
const OFFSET = offset !== undefined ? ` OFFSET ${offset}` : " OFFSET 0";
const LIMIT = limit !== undefined ? ` LIMIT ${limit}` : "";
queryText = queryText + OFFSET + LIMIT;
}
const querySpec: SqlQuerySpec = {
query: queryText,
parameters: params,
};
console.info("querySpec:", querySpec);
return querySpec;
};
export type Param = {
name: string;
value: Json;
};
export type FilterResult = {
queries: string[];
params: Param[];
};
/**
* generate query text and params for filter part.
*
* e.g. {"count >": 10} -> {queries: ["count > @count_xxx"], params: [{name: "@count_xxx", value: 10}]}
*
* @param _filter
*/
export const _generateFilter = (_filter: JsonObject | undefined): FilterResult => {
// undefined filter
if (!_filter) {
return { queries: [], params: [] };
}
// normalize the filter
const filter = _flatten(_filter);
// process binary expressions {"count >": 10, "lastName !=": "Banks", "firstName CONTAINS"}
let queries: string[] = [];
let params: { name: string; value: Json }[] = [];
Object.keys(filter).forEach((k) => {
const exp = parse(k, filter[k]);
const { queries: expQueries, params: expParams } = exp.toFilterResult();
queries = queries.concat(expQueries);
params = params.concat(expParams);
});
return { queries, params };
};
/**
* flatten an object to a flat "obj1.key1.key2" format
*
* e.g. {obj1 : { key1 : { key2 : "test"}}} -> {obj1: {"key1.key2": "test"}}
*
* @param obj
* @param result
* @param keys
*/
export const _flatten = (
obj?: JsonObject,
result: JsonObject = {},
keys: string[] = [],
): JsonObject => {
if (!obj) {
return {};
}
Object.keys(obj).forEach((k) => {
keys.push(k);
const childObj = obj[k];
if (isJsonObject(childObj)) {
_flatten(childObj, result, keys);
} else if (childObj !== undefined) {
result[keys.join(".")] = obj[k];
}
keys.pop();
});
return result;
};
/**
* Instead of c.key, return c["key"] or c["key1"]["key2"] for query. In order for cosmosdb reserved words
*
* @param key filter's key
* @param collectionAlias default to "c", can be "x" when using subquery for EXISTS or JOIN
* @return formatted filter's key c["key1"]["key2"]
*/
export const _formatKey = (key: string, collectionAlias = "r"): string => {
assertNotEmpty(collectionAlias, "collectionAlias");
if (!key) {
// return collectionAlias when key is empty
return collectionAlias;
}
return key
.split(".")
.reduce(
(r, f) => {
r.push(`["${f}"]`);
return r;
},
[collectionAlias],
)
.join("");
};