typeorm-hasura 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.env.example +9 -0
  2. package/LICENSE +21 -0
  3. package/dev-playground/UserRole.ts +5 -0
  4. package/dev-playground/action/currencyConverter.ts +60 -0
  5. package/dev-playground/data-source.ts +21 -0
  6. package/dev-playground/entity/Org.ts +52 -0
  7. package/dev-playground/entity/Product.ts +74 -0
  8. package/dev-playground/entity/User.ts +51 -0
  9. package/dev-playground/entity/index.ts +3 -0
  10. package/dev-playground/hasura.ts +35 -0
  11. package/dev-playground/index.ts +43 -0
  12. package/dev-playground/migration/1679166386871-next.ts +29 -0
  13. package/jest.config.js +6 -0
  14. package/package.json +70 -0
  15. package/random-notes.md +8 -0
  16. package/rollup.config.mjs +56 -0
  17. package/src/builders/Action.ts +27 -0
  18. package/src/builders/Metadata.ts +96 -0
  19. package/src/builders/index.ts +2 -0
  20. package/src/decorators/Column.ts +12 -0
  21. package/src/decorators/Entity.ts +12 -0
  22. package/src/decorators/index.ts +2 -0
  23. package/src/index.ts +5 -0
  24. package/src/internalStorage.ts +23 -0
  25. package/src/mappers/databaseUrl.spec.ts +33 -0
  26. package/src/mappers/databaseUrl.ts +19 -0
  27. package/src/mappers/graphql.spec.ts +124 -0
  28. package/src/mappers/graphql.ts +61 -0
  29. package/src/mappers/hasuraKind.spec.ts +12 -0
  30. package/src/mappers/hasuraKind.ts +11 -0
  31. package/src/mappers/index.ts +3 -0
  32. package/src/mappers/permissions.spec.ts +143 -0
  33. package/src/mappers/permissions.ts +84 -0
  34. package/src/mappers/relationships.ts +65 -0
  35. package/src/mappers/source.ts +29 -0
  36. package/src/mappers/table.ts +27 -0
  37. package/src/mappers/tableConfiguration.ts +45 -0
  38. package/src/mappers/whereClause.spec.ts +85 -0
  39. package/src/mappers/whereClause.ts +41 -0
  40. package/src/types/Action.ts +27 -0
  41. package/src/types/Column.ts +23 -0
  42. package/src/types/DataSourceOptions.ts +22 -0
  43. package/src/types/Entity.ts +53 -0
  44. package/src/types/base.ts +2 -0
  45. package/src/types/index.ts +7 -0
  46. package/src/types/permissions.ts +15 -0
  47. package/src/types/whereClause.ts +75 -0
  48. package/tsconfig.json +26 -0
@@ -0,0 +1,65 @@
1
+ import * as TypeORM from "typeorm";
2
+ import type * as Hasura from "hasura-metadata-types";
3
+
4
+ type RelationshipKind = 'object_relationships' | 'array_relationships'
5
+
6
+ export function generateRelationship(relation: TypeORM.EntityMetadata['relations'][number]): {
7
+ kind: RelationshipKind,
8
+ relationship: Hasura.LocalTableObjectRelationship | Hasura.SameTableObjectRelationship
9
+ } {
10
+ const kind = relation.relationType.endsWith('-to-one') ? 'object_relationships' : 'array_relationships';
11
+
12
+ const owningRelation = relation.isOwning ? relation : relation.inverseRelation;
13
+
14
+ if (!owningRelation)
15
+ throw new Error('Does not support many-to-many relations yet, so we will skip this specific relation. ' +
16
+ 'Also its possible that you have missed to set inverse side of the relation.');
17
+
18
+ const columns = owningRelation.joinColumns.map(column => column.propertyName);
19
+
20
+ // todo: does not work?
21
+ // const schema = owningRelation.entityMetadata.schema;
22
+ // @ts-ignore is that okay?
23
+ const schema = owningRelation.target.dataSource.options.schema || 'public';
24
+
25
+ const relationship: Hasura.LocalTableObjectRelationship | Hasura.SameTableObjectRelationship =
26
+ relation.isOwning ? {
27
+ name: relation.propertyName,
28
+ using: {
29
+ foreign_key_constraint_on: columns
30
+ }
31
+ } : {
32
+ name: relation.propertyName,
33
+ using: {
34
+ foreign_key_constraint_on: {
35
+ columns,
36
+ table: {
37
+ name: owningRelation.entityMetadata.tableName,
38
+ schema,
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ return { kind, relationship };
45
+ }
46
+
47
+ export function generateRelationships(relations: TypeORM.EntityMetadata['relations']):
48
+ Pick<Hasura.MetadataTable, RelationshipKind> {
49
+ const result: Required<Pick<Hasura.MetadataTable, RelationshipKind>> = {
50
+ object_relationships: [],
51
+ array_relationships: [],
52
+ }
53
+
54
+ for (const relation of relations) {
55
+ try {
56
+ const { kind, relationship } = generateRelationship(relation);
57
+ // @ts-ignore dont want to play with types for now
58
+ result[kind].push(relationship);
59
+ } catch (e) {
60
+ console.warn(e);
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
@@ -0,0 +1,29 @@
1
+ import type * as Hasura from "hasura-metadata-types";
2
+ import { DataSourceOptions } from "../types";
3
+ import { getHasuraKind } from "./hasuraKind";
4
+ import { getDatabaseUrl } from "./databaseUrl";
5
+ import { generateTable } from "./table";
6
+
7
+ export function generateSource(dataSourceOptions: DataSourceOptions): Hasura.Source {
8
+ let { name, dataSource, customizationNative: customization } = dataSourceOptions;
9
+
10
+ // customization ??= {}
11
+ // customization.naming_convention ??= 'graphql-default'
12
+
13
+ return {
14
+ name,
15
+ kind: getHasuraKind(dataSource.options.type),
16
+ tables: [...dataSource.entityMetadatas]
17
+ .reverse()
18
+ .map(table => generateTable(dataSourceOptions, table)),
19
+ customization,
20
+ configuration: {
21
+ "connection_info": {
22
+ "database_url": getDatabaseUrl(dataSourceOptions),
23
+ "isolation_level": "read-committed",
24
+ "use_prepared_statements": false
25
+ },
26
+ "extensions_schema": "extensions_schema_test"
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,27 @@
1
+ import type * as Hasura from "hasura-metadata-types";
2
+ import * as TypeORM from "typeorm";
3
+ import { generateRelationships } from "./relationships";
4
+ import { generateTableConfiguration } from "./tableConfiguration";
5
+ import { DataSourceOptions } from "../types";
6
+ import { generatePermissions } from "./permissions";
7
+ import { internalStorage } from "../internalStorage";
8
+
9
+ export function generateTable<Entity extends Object>(
10
+ dataSourceOptions: DataSourceOptions,
11
+ table: TypeORM.EntityMetadata
12
+ ): Hasura.MetadataTable {
13
+ const entityOptions = internalStorage.getEntityOptions<Entity>(table.target);
14
+ const columnMetadata = internalStorage.getEntityColumnsOptionsList(table.target);
15
+
16
+ return {
17
+ table: {
18
+ name: table.tableName,
19
+ // todo: does not work?
20
+ // schema: table.schema,
21
+ schema: 'schema' in table.connection.options && table.connection.options.schema || 'public',
22
+ },
23
+ configuration: generateTableConfiguration(table, entityOptions, columnMetadata),
24
+ ...generateRelationships(table.relations),
25
+ ...generatePermissions(dataSourceOptions, entityOptions, columnMetadata),
26
+ }
27
+ }
@@ -0,0 +1,45 @@
1
+ import * as TypeORM from "typeorm";
2
+ import type * as Hasura from "hasura-metadata-types";
3
+ import { ColumnMetadata, EntityOptions, EntityRootField } from "../types";
4
+ import snakeCase from 'lodash.snakecase'
5
+
6
+ export function generateTableConfiguration<Entity extends Object>(table: TypeORM.EntityMetadata,
7
+ entityOptions: EntityOptions<Entity> | undefined,
8
+ columnMetadata: ColumnMetadata[]
9
+ ): Hasura.MetadataTableConfig {
10
+
11
+ let custom_name: string | undefined = entityOptions && entityOptions.customName || table.tableName;
12
+
13
+ const columnHasuraEntries: [string, Hasura.MetadataTableColumnConfig][] = [];
14
+ for (const column of columnMetadata) {
15
+ // looks like its only possible to set custom_name for columns here
16
+ if (column.options?.customName) {
17
+ columnHasuraEntries.push([column.propertyName, {
18
+ custom_name: column.options.customName,
19
+ }])
20
+ }
21
+ }
22
+ const column_config = columnHasuraEntries.length ? Object.fromEntries(columnHasuraEntries) : undefined;
23
+
24
+ const custom_root_fields: Record<string, EntityRootField> = {};
25
+ if (entityOptions?.customRootFields) {
26
+ // iterate over all possible root fields and push them to custom_root_fields
27
+ for (const rootField in entityOptions.customRootFields) {
28
+ let rootFieldConfig = entityOptions.customRootFields[rootField as keyof typeof entityOptions.customRootFields];
29
+ if (rootFieldConfig) {
30
+ if (typeof rootFieldConfig === 'string') {
31
+ rootFieldConfig = {
32
+ name: rootFieldConfig,
33
+ };
34
+ }
35
+ custom_root_fields[snakeCase(rootField)] = rootFieldConfig;
36
+ }
37
+ }
38
+ }
39
+
40
+ return {
41
+ custom_name,
42
+ column_config,
43
+ custom_root_fields,
44
+ }
45
+ }
@@ -0,0 +1,85 @@
1
+ import { Equal, Not, MoreThan, LessThan, MoreThanOrEqual, LessThanOrEqual, Like, ILike, In, BaseEntity } from "typeorm"
2
+
3
+ import { convertWhereClause } from "./whereClause"
4
+ import { User, Org, Product } from "../../dev-playground/entity";
5
+ import { Where, Filter } from "../types";
6
+
7
+ type Case<T extends BaseEntity> = {
8
+ input: Where<T>;
9
+ output: Filter<T>
10
+ }
11
+
12
+ const cases: Case<User>[] = [
13
+ {
14
+ input: {
15
+ id: "1",
16
+ },
17
+ output: {
18
+ id: { _eq: "1" }
19
+ }
20
+ },
21
+ {
22
+ input: {
23
+ id: "1",
24
+ name: "test"
25
+ },
26
+ output: {
27
+ _and: [
28
+ { id: { _eq: "1" } },
29
+ { name: { _eq: "test" } }
30
+ ]
31
+ }
32
+ },
33
+ {
34
+ input: [
35
+ { id: "1", name: "test" },
36
+ { id: "2", name: "test" }
37
+ ],
38
+ output: {
39
+ _or: [
40
+ { _and: [{ id: { _eq: "1" } }, { name: { _eq: "test" } }] },
41
+ { _and: [{ id: { _eq: "2" } }, { name: { _eq: "test" } }] }
42
+ ]
43
+ }
44
+ },
45
+ {
46
+ input: {
47
+ id: Not("1")
48
+ },
49
+ output: {
50
+ id: { _neq: "1" }
51
+ }
52
+ },
53
+ {
54
+ input: [
55
+ { id: Not("1"), name: "test" },
56
+ { id: "2", name: In(["test"]) }
57
+ ],
58
+ output: {
59
+ _or: [
60
+ { _and: [{ id: { _neq: "1" } }, { name: { _eq: "test" } }] },
61
+ { _and: [{ id: { _eq: "2" } }, { name: { _in: ["test"] } }] }
62
+
63
+ ],
64
+
65
+ },
66
+ },
67
+ {
68
+ input: {
69
+ products: {
70
+ id: "1"
71
+ }
72
+ },
73
+ output: {
74
+ products: {
75
+ id: { _eq: "1" },
76
+ }
77
+ }
78
+ }
79
+ ]
80
+
81
+ describe("convert whereTypeorm to hasuraObj", () => {
82
+ cases.forEach(({ input, output }) =>
83
+ it("input to Equal output", () => expect(convertWhereClause(input)).toEqual(output))
84
+ )
85
+ })
@@ -0,0 +1,41 @@
1
+ import { FindOptionsWhere, InstanceChecker, And } from "typeorm";
2
+ import { Where, Filter, Operators, } from "../types";
3
+
4
+ export function convertWhereClause<Entity extends Object>(...wheres: (Where<Entity> | undefined)[]): Filter<Entity> {
5
+ wheres = wheres.filter(Boolean)
6
+ if (!wheres.length) return {}
7
+ if (wheres.length > 1) {
8
+ return {
9
+ _and: wheres.map(where => convertWhereClause(where))
10
+ }
11
+ }
12
+ const [where] = wheres
13
+
14
+ if (!where) return {}
15
+ if (Array.isArray(where)) {
16
+ return { _or: where.map(i => parseParameters(i)) }
17
+ }
18
+ return parseParameters(where)
19
+ }
20
+
21
+ function parseParameters<Entity extends Object>(object: FindOptionsWhere<Entity>): Filter<Entity> {
22
+ let conditions: Filter<Entity>[] = []
23
+ for (let key in object) {
24
+ const parameterValue = object[key]
25
+ if (InstanceChecker.isFindOperator(parameterValue)) {
26
+ const operator = Operators[parameterValue.type]
27
+ if (operator) {
28
+ conditions.push({ [key]: { [operator]: parameterValue.value } })
29
+ } else
30
+ throw new Error("this operator is not supported in this time");
31
+ } else if (["string", "number", "boolean"].includes(typeof parameterValue)) {
32
+ conditions.push({ [key]: { "_eq": parameterValue } })
33
+ } else if (typeof parameterValue === "object" && !Array.isArray(parameterValue) && parameterValue !== null) {
34
+ conditions.push({ [key]: parseParameters(parameterValue) })
35
+ } else
36
+ throw new Error("this parameter is not supported in this time");
37
+ }
38
+ return conditions.length == 0 ? {} :
39
+ conditions.length == 1 ? conditions[0] :
40
+ { _and: conditions }
41
+ }
@@ -0,0 +1,27 @@
1
+ import { DocumentNode } from "graphql"
2
+ import type * as Hasura from "hasura-metadata-types";
3
+ import { UserRoleName } from "./base";
4
+
5
+ export type GraphQlMetadataForAction = {
6
+ baseActions: (Omit<Hasura.Action, 'definition'> & {
7
+ definition: Pick<Hasura.ActionDefinition, 'type' | 'output_type' | 'arguments'>;
8
+ })[],
9
+ custom_types: Hasura.CustomTypes,
10
+ }
11
+
12
+ export type ActionCustomMetadataV1 = {
13
+ comment?: string,
14
+ /**
15
+ * The action's webhook URL
16
+ */
17
+ handler: string,
18
+ definitionType: DocumentNode,
19
+ /**
20
+ * If set to true the client headers are forwarded to the webhook handler (default: false)
21
+ */
22
+ forwardClientHeaders?: boolean
23
+ nativeDefinition: Partial<Hasura.ActionDefinition>,
24
+ permissions?: { role: UserRoleName }[];
25
+ }
26
+
27
+ export type ActionBuildResult = Required<Pick<Hasura.Metadata['metadata'], 'actions' | 'custom_types'>>
@@ -0,0 +1,23 @@
1
+ import { UserActionType, UserRoleName } from "./base";
2
+ import { EntityTarget } from "./Entity";
3
+
4
+
5
+ export interface ColumnOptions {
6
+ /**
7
+ * Column name which will be show up in hasura.
8
+ */
9
+ customName?: string;
10
+
11
+ /**
12
+ * Column type which will be allowed to do actions.
13
+ */
14
+ permissions?: {
15
+ [role: UserRoleName]: UserActionType[] | UserActionType | boolean
16
+ }
17
+ }
18
+
19
+ export interface ColumnMetadata {
20
+ object: EntityTarget,
21
+ propertyName: string,
22
+ options: ColumnOptions | undefined,
23
+ }
@@ -0,0 +1,22 @@
1
+ import * as TypeORM from "typeorm";
2
+ import type * as Hasura from "hasura-metadata-types";
3
+
4
+ export interface DataSourceOptions {
5
+ name: string;
6
+ dataSource: TypeORM.DataSource;
7
+ customizationNative?: Hasura.SourceCustomization;
8
+
9
+ /**
10
+ * override database url instead of url from data source
11
+ *
12
+ * @example `postgres://username:password@host:5432/database`
13
+ * @deprecated please rely on the `dataSource` parameter instead
14
+ */
15
+ databaseUrl?: string;
16
+
17
+ /**
18
+ * Set limit on number of rows fetched per request by default for all entities
19
+ * @default undefined
20
+ */
21
+ defaultSelectPermissionLimit?: number;
22
+ }
@@ -0,0 +1,53 @@
1
+ import { UserActionType, UserRoleName } from "./base";
2
+ import { BasePermissionRule, SelectPermissionRule } from "./permissions";
3
+ import { Where } from "./whereClause";
4
+
5
+ export type EntityTarget = any;
6
+
7
+ export interface EntityRootField {
8
+ /**
9
+ * name for root field
10
+ */
11
+ name?: string;
12
+ /**
13
+ * user visible comment for root field
14
+ */
15
+ comment?: string;
16
+ }
17
+
18
+ export type Permissions<Entity extends Object> = {
19
+ [role: UserRoleName]: {
20
+ /**
21
+ * append where clause to all actions
22
+ */
23
+ where?: Where<Entity>;
24
+ select?: SelectPermissionRule<Entity> | boolean;
25
+ } & {
26
+ [action in Exclude<UserActionType, 'select'>]?: BasePermissionRule<Entity> | boolean;
27
+ }
28
+ }
29
+
30
+ export interface EntityOptions<Entity extends Object = Object> {
31
+ /**
32
+ * Table name which will be show up in hasura.
33
+ */
34
+ customName?: string;
35
+
36
+ customRootFields?:
37
+ Partial<Record<
38
+ | 'select'
39
+ | 'selectByPk'
40
+ | 'selectAggregate'
41
+ | 'selectStream'
42
+ | 'insert'
43
+ | 'insertOne'
44
+ | 'update'
45
+ | 'updateByPk'
46
+ | 'delete'
47
+ | 'deleteByPk'
48
+ | 'updateMany'
49
+ , EntityRootField | string>>,
50
+
51
+ permissions?: Permissions<Entity>
52
+ }
53
+
@@ -0,0 +1,2 @@
1
+ export type UserRoleName = string;
2
+ export type UserActionType = 'select' | 'insert' | 'update' | 'delete';
@@ -0,0 +1,7 @@
1
+ export * from './Action'
2
+ export * from './base'
3
+ export * from './Column'
4
+ export * from './DataSourceOptions'
5
+ export * from './Entity'
6
+ export * from './permissions'
7
+ export * from './whereClause'
@@ -0,0 +1,15 @@
1
+ import { Where, Filter } from "./whereClause";
2
+ export interface BasePermissionRule<Entity extends Object> {
3
+ where?: Where<Entity>;
4
+ }
5
+
6
+ export interface SelectPermissionRule<Entity extends Object> extends BasePermissionRule<Entity> {
7
+ /**
8
+ * limit the number of rows returned by the query(select action only)
9
+ */
10
+ limit?: number;
11
+ /**
12
+ * allow aggregations on the table (select action only)
13
+ */
14
+ allowAggregations?: boolean;
15
+ }
@@ -0,0 +1,75 @@
1
+ import { FindOptionsWhere, FindOperatorType, BaseEntity } from "typeorm";
2
+
3
+ export type Where<Entity> = FindOptionsWhere<Entity>[] | FindOptionsWhere<Entity>
4
+
5
+ export type ExclusiveKeys<T extends {}, Key = { [K in keyof T]: { [P in keyof T]?: P extends K ? T[P] : never } }> =
6
+ { [K in keyof Key]: Key[K] }[keyof Key];
7
+
8
+ type StringParameters =
9
+ | "_eq"
10
+ | "_neq"
11
+ | "_gt"
12
+ | "_lt"
13
+ | "_gte"
14
+ | "_lte"
15
+ | "_like"
16
+ // | "_nlike"
17
+ | "_ilike"
18
+ // | "_nilike"
19
+ // | "_similar"
20
+ // | "_nsimilar"
21
+ // | "_regex"
22
+ // | "_iregex"
23
+ // | "_nregex"
24
+ // | "_niregex"
25
+
26
+ type ArrayParameters =
27
+ | "_in"
28
+ // | "_nin"
29
+ // | "_ceq"
30
+ // | "_cne"
31
+ // | "_cgt"
32
+ // | "_clt"
33
+ // | "_cgte"
34
+ // | "_clte"
35
+ type NullParameters = "_is_null"
36
+ type JsonBParameters = "_contains" | "_contained_in" | "_has_key" | "_has_keys_any" | "_has_keys_all"
37
+
38
+ type ExclusiveParameters = ExclusiveKeys<
39
+ { [key in StringParameters]?: string; } &
40
+ { [key in ArrayParameters]?: string[] } &
41
+ { [key in NullParameters]?: boolean }
42
+ >
43
+ type Subject<Entity extends Object> = {
44
+ [key in keyof Entity]?: ExclusiveParameters
45
+ }
46
+ export type ExclusiveArguments<Entity extends Object, T = {}, EntityParameters extends string | number | symbol = keyof Omit<Entity, keyof BaseEntity>> =
47
+ ExclusiveKeys<{ [key in EntityParameters]?: ExclusiveParameters | Subject<Entity> } & T>
48
+
49
+ export type Filter<EntityParameters extends Object> = ExclusiveKeys<{
50
+ _and?: Filter<EntityParameters>[];
51
+ _or?: Filter<EntityParameters>[];
52
+ _not?: Filter<EntityParameters>
53
+ }> | ExclusiveArguments<EntityParameters>
54
+
55
+ export const Operators: Readonly<
56
+ Partial<Record<FindOperatorType, StringParameters | ArrayParameters>>
57
+ > = {
58
+ // string
59
+ "equal": "_eq",
60
+ "not": "_neq",
61
+ "moreThan": "_gt",
62
+ "lessThan": "_lt",
63
+ "moreThanOrEqual": "_gte",
64
+ "lessThanOrEqual": "_lte",
65
+ "like": "_like",
66
+ "ilike": "_ilike",
67
+ // array
68
+ "in": "_in",
69
+ }
70
+ enum NullParams {
71
+ "isNull" = "_is_null"
72
+ }
73
+
74
+ // all typeorm operators FindOperatorType
75
+
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "es6",
5
+ "module": "ESNext",
6
+ "moduleResolution": "node",
7
+ "outDir": "./dist",
8
+ "emitDecoratorMetadata": true,
9
+ "experimentalDecorators": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "sourceMap": true,
12
+ "types": [
13
+ "node",
14
+ "jest"
15
+ ],
16
+ "rootDir": "./src"
17
+ },
18
+ "include": [
19
+ "./src/**/*.ts"
20
+ ],
21
+ "exclude": [
22
+ "node_modules",
23
+ "build",
24
+ "**/*.spec.ts"
25
+ ]
26
+ }