Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

📝 following beta format X.Y.Z where Y = breaking change and Z = feature and fix. Later => FAIL.FEATURE.FIX

## 0.11.0(2024-09-01)

- Feat: Working queries and basic mutations for surrealDB

## 0.10.31(2024-07-31)

Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blitznocode/blitz-orm",
"version": "0.10.31",
"version": "0.11.0",
"author": "blitznocode.com",
"description": "Blitz-orm is an Object Relational Mapper (ORM) for graph databases that uses a JSON query language called Blitz Query Language (BQL). BQL is similar to GraphQL but uses JSON instead of strings. This makes it easier to build dynamic queries.",
"main": "dist/index.mjs",
Expand Down Expand Up @@ -30,11 +30,13 @@
"test:ignoreTodo": "pnpm test:surrealdb-ignoreTodo && pnpm test:typedb-ignoreTodo && pnpm test:multidb",
"test:multidb": "./tests/test.sh multidb",
"test:query": "./tests/test.sh query.test.ts",
"test:surrealdb-ignoreTodo": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=refs ./tests/test.sh tests/unit/allTests.test.ts -t \"^(?!.*TODO{.*[S].*}:).*\"",
"test:surrealdb-ignoreTodo:edges": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=edges ./tests/test.sh tests/unit -t \"^(?!.*TODO{.*[S].*}:).*\"",
"test:surrealdb-ignoreTodo:refs": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=refs ./tests/test.sh tests/unit -t \"^(?!.*TODO{.*[S].*}:).*\"",
"test:surrealdb-query:edges": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=edges ./tests/test.sh tests/unit/queries/query.test.ts",
"test:surrealdb-query:refs": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=refs ./tests/test.sh tests/unit/queries/query.test.ts",
"test:typedb-ignoreTodo": "cross-env BORM_TEST_ADAPTER=typeDB vitest run tests/unit -t \"^(?!.*TODO{.*[T].*}:).*\" ",
"test:surrealdb-mutation:edges": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=edges ./tests/test.sh tests/unit/mutations",
"test:surrealdb-mutation:refs": "cross-env BORM_TEST_ADAPTER=surrealDB BORM_TEST_SURREALDB_LINK_MODE=refs ./tests/test.sh tests/unit/mutations",
"test:typedb-ignoreTodo": "cross-env BORM_TEST_ADAPTER=typeDB vitest run tests/unit/allTests.test.ts -t \"^(?!.*TODO{.*[T].*}:).*\" ",
"test:typedb-mutation": "cross-env BORM_TEST_ADAPTER=typeDB vitest run unit/mutations",
"test:typedb-query": "cross-env BORM_TEST_ADAPTER=typeDB vitest run tests/unit/queries --watch",
"test:typedb-schema": "cross-env BORM_TEST_ADAPTER=typeDB vitest run unit/schema",
Expand Down Expand Up @@ -65,8 +67,8 @@
"object-traversal": "^1.0.1",
"radash": "^12.1.0",
"robot3": "^0.4.1",
"surrealdb.js": "1.0.0-beta.8",
"typedb-driver": "2.28.2-rc1",
"surrealdb.js": "1.0.0-beta.9",
"typedb-driver": "2.28.4",
"uuid": "^9.0.1"
},
"devDependencies": {
Expand Down
38 changes: 29 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Blitz-orm

Blitz-orm is an Object Relational Mapper (ORM) for graph databases that uses a JSON query language called Blitz Query Language (BQL). BQL is similar to GraphQL but uses JSON instead of strings. This makes it easier to build dynamic queries.

Blitz-orm is similar to other ORM packages such as Prisma. You define a BQL schema and it gets translated to different databases (currently only compatible with TypeDB but a dgraph adapter in the oven).

## Compatibility
Currently, the only database that is compatible with Blitz-orm is TypeDB. The goal is to build adapters for other graph databases such as Dgraph and Neo4j, as well as classic databases like PostgreSQL and MongoDB in the future.

Currently, it works with TypeDB and surrealDB

## How to Use

1. Install the package using your package manager, for example:
`yarn add @blitznocode/blitz-orm`
2. Create a Borm schema. You can find an example in the test folder.
3. The borm.define() function is currently not working, so you will need to manually translate your BQL schema into a TypeQL schema (an example can be found in the test folder).
4. Create a configuration file with the database name that you have created in TypeDB.
5. Initialize Blitz-orm in a file like this:
```

```ts
import BormClient from '@blitznocode/blitz-orm';

import { bormConfig } from './borm.config';
Expand All @@ -26,55 +30,71 @@ const bormClient = new BormClient({

export default bormClient;
```

6. You can then run queries and mutations like this:
```

```ts
const res = await bormClient.mutate({$entity: 'User', name: 'Ann'}, { noMetadata: true });
```

## Gotchas

1) There is no borm.define() method yet. This means you will need to translate your borm schema into typeQL schema manually
2) Private (non shared) attributes are defined in typeDB as "nameOfTheThing·nameOfTheAttribute", where "·" is a mid-do. As an example:
```

```t
#shared attribute (shared: true) :
title sub attribute, value string;
#as a private attribute (shared: false), default behaviour:
book·title sub attribute, value string;
```

## Documentation & example queries

You can find example mutations and queries in the tests
There is no official documentation but you can check the draft RFC:
https://www.notion.so/blitzapps/BlitzORM-RFC-eb4a5e1464754cd7857734eabdeaa73c

The RFC includes future features and is not updated so please keep an eye on the query and mutation tests as those are designed for the features already working.

## How to Run TypeDB Locally

To run TypeDB locally, follow the official instructions at https://docs.vaticle.com/docs/running-typedb/install-and-run. It is recommended to run TypeDB Studio, define the schema there, and test with pure TypeQL before using Blitz-orm.

## Collaboration & Contact

You can contribute to the project by adding adapters for other databases, developing a BQL-to-GraphQL mapper, enhancing performance, or contributing to the public roadmap for this package (not yet published). To get in touch, please send an email to [email protected].

## Warning

Blitz-orm is currently in alpha version and not ready for production use. Some key queries and mutations do work, but there is still much that needs to be done and performance improvements are needed. One of the biggest performance issues is with nested queries, as they currently require a call to TypeDB for each level of depth.

## What is Currently Working

To see what is currently working and find examples, please check the test folder, where you will find a variety of queries and mutations.

## TypeGen

This orm includes a basic typeGen that gets you types depending on the structure of the borm Schema. You can use it like this:
```
type UserType = GenerateType<typeof typesSchema.relations.User>;

```ts
type UserType = GenerateType<typeof typesSchema.relations.User>;
```

Due to typescript limitations and also to be able to type fields from extended things, you will need to compile your bormSchema to a particular format. In order to make this work you can see the example that we have in the tests that you can run with `pnpm test:buildSchema`.

You can also use it with your base schema without compiling but some fields might not be there and you might need to ignore some ts errors. Also you will need "as const" at the end of your schema.

## The future of this package
- Achieve 100% compatibility with typeDB functions
- Enhance functionality with new features such as, cardinality management, ordered attributes, Vectors (ordered relations)...

- Achieve 100% compatibility with typeDB and surrealDB
- Automatic schemas: Transform BQL schemas into any schema
- GraphQL compatibility
- Enhance functionality with new features such as ordered attributes, Vectors (ordered relations)...
- Expand compatibility to other graph databases and traditional databases such as PostgreSQL or MongoDB
- Enable the ability to split queries and mutations across multiple databases (for example, some data stored in PostgreSQL and other data in typeQL, all queried from a single point)
- Expand multidb compatibility, as now it is possible only for basic queries (querying data from two different DBs in a single BQL query)

## Development

- We use pnpm as a package manager
- You will need to add "-w", for instance `pnpm add -D husky -w`
4 changes: 2 additions & 2 deletions src/adapters/surrealDB/enrichSchema/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const getSurrealLinkFieldQueryPath = ({
(withExtensionsSchema.relations[linkField.relation] as EnrichedBormRelation).subTypes || [];
const targetRelationThings = [linkField.relation, ...targetRelationSubTypes];

const pathToRelation = `<-\`${originalRelation}_${linkField.plays}\`<-(\`${targetRelationThings.join('`,`')}\`)`;
const pathToRelation = `<-${originalRelation}_${linkField.plays}<-(${targetRelationThings.join('⟩,⟨')})`;

if (linkField.target === 'relation') {
if (linkMode === 'edges') {
Expand All @@ -42,7 +42,7 @@ export const getSurrealLinkFieldQueryPath = ({

const oppositeRoleThings = [targetRole.thing, ...targetRoleSubTypes];

const pathToTunneledRole = `->\`${originalRelation}_${targetRole.plays}\`->(\`${oppositeRoleThings.join('`,`')}\`)`;
const pathToTunneledRole = `->${originalRelation}_${targetRole.plays}->(${oppositeRoleThings.join('⟩,⟨')})`;

if (linkMode === 'edges') {
return `${pathToRelation}${pathToTunneledRole}`;
Expand Down
183 changes: 183 additions & 0 deletions src/adapters/surrealDB/filters/filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { isArray, isObject, shake } from 'radash';
import { getFieldType } from '../../../helpers';
import type { Filter, EnrichedBormSchema, EnrichedLinkField, EnrichedRoleField } from '../../../types';
import { SuqlMetadata } from '../../../types/symbols';

export const parseFilter = (filter: Filter, currentThing: string, schema: EnrichedBormSchema): Filter => {
if (filter === null || filter === undefined) {
return filter;
}

const wasArray = isArray(filter);

const arrayFilter = wasArray ? filter : [filter];

const resultArray = arrayFilter.map((f) => {
const keys = Object.keys(f);

const result = keys.reduce((acc, key) => {
const value = f[key];

if (key.startsWith('$')) {
if (key === '$not') {
return { ...acc, $not: undefined, ['$!']: parseFilter(value, currentThing, schema) };
}

if (key === '$or') {
return { ...acc, $or: undefined, $OR: parseFilter(value, currentThing, schema) };
}

if (key === '$and') {
return { ...acc, $and: undefined, $AND: parseFilter(value, currentThing, schema) };
}

if (key === '$eq') {
return { ...acc, '$nor': undefined, '$=': parseFilter(value, currentThing, schema) };
}

if (key === '$id') {
return { ...acc, '$id': undefined, 'record::id(id)': { $IN: isArray(value) ? value : [value] } };
}

if (key === '$thing') {
return acc; //do nothing for now, but in the future we will need to filter by tables as well, maybe record::tb(id) ...
}

return { ...acc, [key]: parseFilter(value, currentThing, schema) };
}

const currentSchema =
currentThing in schema.entities ? schema.entities[currentThing] : schema.relations[currentThing];

const [fieldType, fieldSchema] = getFieldType(currentSchema, key);

if (fieldType === 'dataField') {
if (currentSchema.idFields.length > 1) {
throw new Error('Multiple id fields not supported');
} //todo: When composed id, this changes:

if (key === currentSchema.idFields[0]) {
return { ...acc, 'record::id(id)': { $IN: isArray(value) ? value : [value] } };
}

return { ...acc, [key]: value }; //Probably good place to add ONLY and other stuff depending on the fieldSchema
}

if (fieldType === 'linkField' || fieldType === 'roleField') {
const fieldSchemaTyped = fieldSchema as EnrichedLinkField | EnrichedRoleField;

if (fieldSchemaTyped.$things.length !== 1) {
throw new Error(
`Not supported yet: Role ${key} in ${value.name} is played by multiple things: ${fieldSchemaTyped.$things.join(', ')}`,
);
}

const [childrenThing] = fieldSchemaTyped.$things; //todo: multiple players, then it must be efined

const surrealDBKey = fieldSchemaTyped[SuqlMetadata].queryPath;

return { ...acc, [surrealDBKey]: parseFilter(value, childrenThing, schema) };
}

throw new Error(`Field ${key} not found in schema, Defined in $filter`);
}, {});

return shake(result);
});

return wasArray ? resultArray : resultArray[0];
};

export const buildSuqlFilter = (filter: object) => {
if (filter === null || filter === undefined) {
return '';
}

const entries = Object.entries(filter);

const parts: string[] = [];

entries.forEach(([key, value]) => {
//TODO: probably better to do it by key first, instead of filtering by the type of value, but it works so to refacto once needed.

if (['$OR', '$AND', '$!'].includes(key)) {
const logicalOperator = key.replace('$', '');

const nestedFilters = Array.isArray(value) ? value.map((v) => buildSuqlFilter(v)) : [buildSuqlFilter(value)];

if (logicalOperator === '!') {
parts.push(`!(${nestedFilters.join(` ${logicalOperator} `)})`);
} else {
parts.push(`(${nestedFilters.join(` ${logicalOperator} `)})`);
}

return;
}

if (isObject(value)) {
if (key.includes('<-') || key.includes('->')) {
const nestedFilter = buildSuqlFilter(value);

parts.push(`${key}[WHERE ${nestedFilter}]`);
} else if (key.startsWith('$parent')) {
//mode: computed refs

const nestedFilter = buildSuqlFilter(value);

const keyWithoutPrefix = key.replace('$parent.', '');

parts.push(`${keyWithoutPrefix}[WHERE ${nestedFilter}]`);
} else if (key.startsWith('$')) {
throw new Error(`Invalid key ${key}`);
} else {
if (Object.keys.length === 1 && Object.keys(value)[0].startsWith('$')) {
// This is the case where the filter has an operator manually defined

const [operator] = Object.keys(value);

//@ts-expect-error its ok, single key

const nextValue = value[operator];

if (isArray(nextValue)) {
parts.push(`${key} ${operator.replace('$', '')} [${nextValue.map((v) => `'${v}'`).join(', ')}]`);
} else if (isObject(nextValue)) {
const nestedFilter = buildSuqlFilter(nextValue);

parts.push(`${key} ${operator.replace('$', '')} ${nestedFilter}`);
} else {
parts.push(`${key} ${operator.replace('$', '')} '${nextValue}'`);
}
} else {
throw new Error(`Invalid key ${key}`);
}
}
} else {
if (Array.isArray(value)) {
const operator = key.startsWith('$') ? key.replace('$', '') : 'IN';

parts.push(`${key} ${operator} [${value.map((v) => `'${v}'`).join(', ')}]`);
} else {
const operator = key.startsWith('$') ? key.replace('$', '') : '=';

parts.push(`${key} ${operator} '${value}'`);
}
}
});

return parts.join(' AND ');
};

export const buildSorter = (sort: ({ field: string; desc?: boolean } | string)[]) => {
const sorters = sort.map((i) => {
if (typeof i === 'string') {
return i;
}

const { field, desc } = i;

return `${field}${desc ? ' DESC' : ' ASC'}`;
});

return `ORDER BY ${sorters.join(', ')}`;
};
11 changes: 7 additions & 4 deletions src/adapters/surrealDB/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ const specialChars = [
'`',
];

export const prepareTableNameSurrealDB = (tableName: string) => {
export const sanitizeNameSurrealDB = (name: string) => {
//if tableName includes any of the chars of this array, then wrap it in backticks

if (specialChars.some((char) => tableName.includes(char))) {
return `⟨\`${tableName}\`⟩`;
if (specialChars.some((char) => name.includes(char))) {
return `⟨${name}⟩`;
}
return tableName;
return name;
};

export const tempSanitizeVarSurrealDB = (input: string): string =>
input.replace(/[ \-+*/=!@#$%^&()\[\]{}|\\;:'"<>,.?~`]/g, '');
8 changes: 4 additions & 4 deletions src/adapters/surrealDB/index.ts.old
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const getSubtype = (
};

const convertEntityId = (attr: EnrichedBqlQueryAttribute) => {
return attr['$path'] === 'id' ? `meta::id(${attr['$path']}) as id` : `${attr['$path']}`;
return attr['$path'] === 'id' ? `record::id(${attr['$path']}) as id` : `${attr['$path']}`;
};

const handleCardinality = (schema: EnrichedBormSchema, query: EnrichedBqlQuery) => (obj: Record<string, unknown>) => {
Expand Down Expand Up @@ -59,15 +59,15 @@ const buildQuery = (thing: string, query: EnrichedBqlQuery) => {
const relations = query['$fields'].filter((q): q is EnrichedBqlQueryEntity => q['$thingType'] === 'relation');

const relationsQuery = relations.map((relation) => {
return `(SELECT VALUE meta::id(id) as id FROM <-${relation.$thing}_${relation.$plays}<-${relation.$thing}) as \`${relation.$as}\``;
return `(SELECT VALUE record::id(id) as id FROM <-${relation.$thing}_${relation.$plays}<-${relation.$thing}) as \`${relation.$as}\``;
});

const entitiesQuery = entities.map((entity) => {
const role = pascal(entity['$playedBy'].relation);

return query.$thingType === 'relation'
? `(SELECT VALUE meta::id(id) as id FROM ->${role}_${entity['$playedBy']['plays']}.out) as ${entity['$as']}`
: `(SELECT VALUE meta::id(id) as id FROM <-${role}_${entity['$playedBy']['path']}<-${role}->${role}_${entity['$playedBy']['plays']}.out) as ${entity['$as']}`;
? `(SELECT VALUE record::id(id) as id FROM ->${role}_${entity['$playedBy']['plays']}.out) as ${entity['$as']}`
: `(SELECT VALUE record::id(id) as id FROM <-${role}_${entity['$playedBy']['path']}<-${role}->${role}_${entity['$playedBy']['plays']}.out) as ${entity['$as']}`;
});

const filterExpr = query['$filter']
Expand Down
Loading