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,96 @@
1
+ import { ActionBuildResult, DataSourceOptions } from "../types";
2
+ import type * as Hasura from "hasura-metadata-types";
3
+ import { generateSource } from "../mappers";
4
+
5
+ export class MetadataBuilder {
6
+ private _metadata: Hasura.Metadata;
7
+
8
+ constructor() {
9
+ this._metadata = {
10
+ resource_version: 0,
11
+ metadata: {
12
+ version: 3,
13
+ sources: [],
14
+ actions: [],
15
+ custom_types: {
16
+ input_objects: [],
17
+ objects: [],
18
+ scalars: [],
19
+ enums: [],
20
+ },
21
+ // backend_configs?: BackendConfigs;
22
+ // remote_schemas?: RemoteSchema[];
23
+ // query_collections?: QueryCollection[];
24
+ // allowlist?: AllowList[];
25
+ // inherited_roles?: InheritedRole[];
26
+ // cron_triggers?: CronTrigger[];
27
+ // network?: Network;
28
+ // rest_endpoints?: RestEndpoint[];
29
+ // api_limits?: ApiLimits;
30
+ // graphql_schema_introspection?: GraphQLSchemaIntrospection;
31
+
32
+ /**
33
+ * The EE Lite OpenTelemetry settings.
34
+ *
35
+ * ATTENTION: Both Lux and the EE Lite server allow configuring OpenTelemetry. Anyway, this only
36
+ * represents the EE Lite one since Lux stores the OpenTelemetry settings by itself.
37
+ */
38
+ // opentelemetry?: OpenTelemetry;
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Adds a source to the metadata.
45
+ */
46
+ addSource(sourceOptions: DataSourceOptions) {
47
+ const source = generateSource(sourceOptions);
48
+ this._metadata.metadata.sources.push(source)
49
+ return this;
50
+ }
51
+
52
+ /**
53
+ * Adds multiple sources to the metadata.
54
+ */
55
+ addSources(sourceOptions: DataSourceOptions[]) {
56
+ sourceOptions.forEach(sourceOptions => this.addSource(sourceOptions));
57
+ return this;
58
+ }
59
+
60
+
61
+ // .addActions([
62
+ // currencyConverterAction
63
+ // ])
64
+
65
+ /**
66
+ * Adds an action to the metadata.
67
+ */
68
+ addAction(action: ActionBuildResult) {
69
+ const metadata = this._metadata.metadata;
70
+ if(!metadata.actions || !metadata.custom_types?.input_objects) {
71
+ throw new Error("Metadata object is not initialized correctly.");
72
+ }
73
+ metadata.actions.push(...action.actions);
74
+ metadata.custom_types.input_objects?.push(...action.custom_types?.input_objects!);
75
+ metadata.custom_types.objects?.push(...action.custom_types?.objects!);
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Adds multiple actions to the metadata.
81
+ */
82
+ addActions(actions: ActionBuildResult[]) {
83
+ actions.forEach(action => this.addAction(action));
84
+ return this;
85
+ }
86
+
87
+
88
+ /**
89
+ * Returns the metadata.
90
+ *
91
+ * @note This method is not async because we might need to do some async stuff in the future.
92
+ */
93
+ getMetadata(): Hasura.Metadata | Promise<Hasura.Metadata> {
94
+ return this._metadata;
95
+ }
96
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Action'
2
+ export * from './Metadata'
@@ -0,0 +1,12 @@
1
+ import { internalStorage } from "../internalStorage";
2
+ import { ColumnOptions } from "../types";
3
+
4
+ export function Column(options?: ColumnOptions): Function {
5
+ return function (object: Object, propertyName: string) {
6
+ internalStorage.pushColumnOptions(object, propertyName, options);
7
+ };
8
+ }
9
+
10
+ export {
11
+ Column as HasuraColumn,
12
+ }
@@ -0,0 +1,12 @@
1
+ import { internalStorage } from "../internalStorage";
2
+ import { EntityOptions } from "../types";
3
+
4
+ export function Entity<Entity extends Object>(options: EntityOptions<Entity>): ClassDecorator {
5
+ return (target: object) => {
6
+ internalStorage.pushEntityOptions(target, options);
7
+ };
8
+ }
9
+
10
+ export {
11
+ Entity as HasuraEntity,
12
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Column';
2
+ export * from './Entity';
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './builders'
2
+ export * from './decorators'
3
+ export * as mappers from './mappers'
4
+ export * from './types'
5
+ export * from './internalStorage'
@@ -0,0 +1,23 @@
1
+ import { ColumnOptions, EntityOptions, ColumnMetadata, EntityTarget } from "./types";
2
+
3
+ export const internalStorage = {
4
+ entityMetadata: new WeakMap<EntityTarget, EntityOptions<{}>>(),
5
+ pushEntityOptions(target: EntityTarget, options: EntityOptions<{}>) {
6
+ internalStorage.entityMetadata.set(target, options);
7
+ },
8
+ getEntityOptions<Entity extends Object = Object>(target: EntityTarget) {
9
+ return internalStorage.entityMetadata.get(target) as EntityOptions<Entity> | undefined
10
+ },
11
+
12
+ columnMetadata: [] as Array<ColumnMetadata>,
13
+ pushColumnOptions(object: object, propertyName: string, options ?: ColumnOptions) {
14
+ internalStorage.columnMetadata.push({
15
+ object,
16
+ propertyName,
17
+ options,
18
+ });
19
+ },
20
+ getEntityColumnsOptionsList(target: EntityTarget) {
21
+ return internalStorage.columnMetadata.filter(column => column.object.constructor === target);
22
+ },
23
+ }
@@ -0,0 +1,33 @@
1
+
2
+ import { getDatabaseUrl } from "./databaseUrl";
3
+
4
+ describe('mappers', () => {
5
+ describe('databaseUrl', () => {
6
+ it('should return postgres for postgres', () => {
7
+ expect(getDatabaseUrl({
8
+ dataSource: {
9
+ name: 'default',
10
+ // @ts-ignore
11
+ type: 'postgres',
12
+ options: {
13
+ type: 'postgres',
14
+ host: 'localhost',
15
+ port: 5432,
16
+ username: 'test-user',
17
+ password: 'test-password',
18
+ database: 'test-database',
19
+ }
20
+ }
21
+ })).toEqual('postgres://test-user:test-password@localhost:5432/test-database');
22
+ });
23
+ it('should throw for unsupported type', () => {
24
+ expect(() => getDatabaseUrl({
25
+ dataSource: {
26
+ name: 'default',
27
+ // @ts-ignore
28
+ type: 'mysql',
29
+ }
30
+ })).toThrow();
31
+ });
32
+ });
33
+ })
@@ -0,0 +1,19 @@
1
+ import { DataSourceOptions } from "../types";
2
+
3
+ // currently hasura does not support object in database url
4
+ export function getDatabaseUrl({ dataSource, databaseUrl }: DataSourceOptions): string {
5
+ if (databaseUrl) return databaseUrl;
6
+
7
+ const { options } = dataSource;
8
+ // check for postgres
9
+ if (options.type !== "postgres") {
10
+ throw new Error(`Unsupported data source type ${options.type}.`);
11
+ }
12
+ if (options.url) {
13
+ return options.url
14
+ }
15
+ if (typeof options.password !== 'string') {
16
+ throw new Error(`Does not support password as a function.`);
17
+ }
18
+ return `postgres://${options.username}:${options.password}@${options.host}:${options.port}/${options.database}`
19
+ }
@@ -0,0 +1,124 @@
1
+ import gql from "graphql-tag"
2
+ import { getGraphQLDefinitions } from "./graphql"
3
+
4
+ describe("convert graphql document to hasura defs", () => {
5
+ it("Base case", () => expect(getGraphQLDefinitions(gql`
6
+ type Query {
7
+ currencyConverter(CurrencyInfo: ConvertCurrencyInputParams!): ConvertedCurrency
8
+ }
9
+
10
+ input ConvertCurrencyInputParams {
11
+ from: String
12
+ to: String
13
+ amt: Int
14
+ }
15
+
16
+ type ConvertedCurrency {
17
+ date: String
18
+ info: ConvertedCurrencyInfo
19
+ query: ConvertedCurrencyQuery
20
+ result: Float
21
+ success: Boolean
22
+ }
23
+
24
+ type ConvertedCurrencyInfo {
25
+ rate: Float
26
+ }
27
+
28
+ type ConvertedCurrencyQuery {
29
+ amount: Int
30
+ from: String
31
+ to: String
32
+ }
33
+ `)).toEqual({
34
+ baseActions: [
35
+ {
36
+ name: "currencyConverter",
37
+ definition: {
38
+ "type": "query",
39
+ output_type: "ConvertedCurrency",
40
+ "arguments": [
41
+ {
42
+ "name": "CurrencyInfo",
43
+ "type": "ConvertCurrencyInputParams!"
44
+ }
45
+ ],
46
+ }
47
+ }
48
+ ],
49
+ "custom_types": {
50
+ "input_objects": [
51
+ {
52
+ "name": "ConvertCurrencyInputParams",
53
+ "fields": [
54
+ {
55
+ "name": "from",
56
+ "type": "String"
57
+ },
58
+ {
59
+ "name": "to",
60
+ "type": "String"
61
+ },
62
+ {
63
+ "name": "amt",
64
+ "type": "Int"
65
+ }
66
+ ]
67
+ }
68
+ ],
69
+ "objects": [
70
+ {
71
+ "name": "ConvertedCurrency",
72
+ "fields": [
73
+ {
74
+ "name": "date",
75
+ "type": "String"
76
+ },
77
+ {
78
+ "name": "info",
79
+ "type": "ConvertedCurrencyInfo"
80
+ },
81
+ {
82
+ "name": "query",
83
+ "type": "ConvertedCurrencyQuery"
84
+ },
85
+ {
86
+ "name": "result",
87
+ "type": "Float"
88
+ },
89
+ {
90
+ "name": "success",
91
+ "type": "Boolean"
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "name": "ConvertedCurrencyInfo",
97
+ "fields": [
98
+ {
99
+ "name": "rate",
100
+ "type": "Float"
101
+ }
102
+ ]
103
+ },
104
+ {
105
+ "name": "ConvertedCurrencyQuery",
106
+ "fields": [
107
+ {
108
+ "name": "amount",
109
+ "type": "Int"
110
+ },
111
+ {
112
+ "name": "from",
113
+ "type": "String"
114
+ },
115
+ {
116
+ "name": "to",
117
+ "type": "String"
118
+ }
119
+ ]
120
+ },
121
+ ]
122
+ }
123
+ }))
124
+ })
@@ -0,0 +1,61 @@
1
+ import { DocumentNode, FieldDefinitionNode, InputValueDefinitionNode } from "graphql";
2
+ import type * as Hasura from "hasura-metadata-types";
3
+ import { GraphQlMetadataForAction } from "../types";
4
+
5
+ function mapFields(values: readonly InputValueDefinitionNode[] | readonly FieldDefinitionNode[]): Hasura.InputArgument[] {
6
+ return values.map(value => ({
7
+ name: value.name.value,
8
+ // @ts-ignore
9
+ type: value.type.kind === "NonNullType" ? value.type.type.name.value + "!" : value.type.name.value,
10
+ }));
11
+ }
12
+
13
+ export function getGraphQLDefinitions(document: DocumentNode): GraphQlMetadataForAction {
14
+ const result: GraphQlMetadataForAction = {
15
+ baseActions: [],
16
+ custom_types: {
17
+ input_objects: [],
18
+ objects: [],
19
+ },
20
+ };
21
+ for (const definition of document.definitions) {
22
+ // type Query or type Mutation
23
+ if (definition.kind === "ObjectTypeDefinition" && ["Query", "Mutation"].includes(definition.name.value)) {
24
+ if (!definition.fields)
25
+ throw new Error(`No fields found in definition for ${definition.name.value}`);
26
+ for (const field of definition.fields) {
27
+ const type = definition.name.value.toLowerCase();
28
+ if (type !== "query" as const && type !== "mutation" as const)
29
+ throw new Error(`Invalid type ${type} for field ${field.name.value}`);
30
+
31
+ let output_type: string;
32
+ if (field.type.kind === "NamedType") {
33
+ output_type = field.type.name.value;
34
+ } else
35
+ throw new Error(`Invalid type ${field.type.kind} for field ${field.name.value}`);
36
+
37
+ result.baseActions.push({
38
+ name: field.name.value,
39
+ definition: {
40
+ type,
41
+ output_type,
42
+ arguments: mapFields(field.arguments || []),
43
+ }
44
+ })
45
+ }
46
+ continue
47
+ }
48
+
49
+ // InputObjectTypeDefinition or ObjectTypeDefinition
50
+ else if (definition.kind === "InputObjectTypeDefinition" || definition.kind === "ObjectTypeDefinition") {
51
+ const type = definition.kind === "InputObjectTypeDefinition" ? "input_objects" : "objects";
52
+ result.custom_types[type]!.push({
53
+ name: definition.name.value,
54
+ fields: mapFields(definition.fields || []),
55
+ })
56
+ }
57
+
58
+ else throw new Error(`Invalid definition ${definition.kind}`);
59
+ }
60
+ return result;
61
+ }
@@ -0,0 +1,12 @@
1
+ import { getHasuraKind } from "./hasuraKind";
2
+
3
+ describe('mappers', () => {
4
+ describe('hasuraKind', () => {
5
+ it('should return postgres for postgres', () => {
6
+ expect(getHasuraKind('postgres')).toEqual('postgres');
7
+ });
8
+ it('should throw for unsupported type', () => {
9
+ expect(() => getHasuraKind('mysql')).toThrow();
10
+ });
11
+ });
12
+ })
@@ -0,0 +1,11 @@
1
+ import type * as TypeORM from "typeorm";
2
+ import type * as Hasura from "hasura-metadata-types";
3
+
4
+ export function getHasuraKind(type: TypeORM.DataSourceOptions["type"]): Hasura.Source["kind"] {
5
+ switch (type) {
6
+ case "postgres":
7
+ return "postgres";
8
+ default:
9
+ throw new Error(`Unsupported data source type ${type}.`);
10
+ }
11
+ }
@@ -0,0 +1,3 @@
1
+ export * from './databaseUrl'
2
+ export * from './hasuraKind'
3
+ export * from './source'
@@ -0,0 +1,143 @@
1
+ import { EntityOptions, ColumnMetadata, DataSourceOptions } from "../types";
2
+ import { BaseEntity } from "typeorm"
3
+ import { generatePermissions, PermissionResult } from "./permissions"
4
+ import { Org } from "../../dev-playground/entity"
5
+
6
+ type Cases<Entity extends BaseEntity> = {
7
+ input: EntityOptions<Entity>,
8
+ output: PermissionResult
9
+ }
10
+
11
+ const dataSourceOptions = {} as unknown as DataSourceOptions
12
+
13
+ let TestColumns: ColumnMetadata[] = [
14
+ {
15
+ object: 1,
16
+ propertyName: "id",
17
+ options: {
18
+ permissions: {
19
+ user: ["select", "update"]
20
+ }
21
+ }
22
+ }
23
+ ]
24
+
25
+ const cases: Cases<Org>[] = [
26
+ {
27
+ input: {
28
+ permissions: {
29
+ user: {
30
+ where: {
31
+ id: "1"
32
+ },
33
+ select: true,
34
+ }
35
+ }
36
+ },
37
+ output: {
38
+ insert_permissions: [],
39
+ select_permissions: [{
40
+ role: "user",
41
+ permission: {
42
+ columns: ["id"],
43
+ filter: {
44
+ id: { _eq: "1" }
45
+ }
46
+ }
47
+ }],
48
+ update_permissions: [],
49
+ delete_permissions: [],
50
+ }
51
+ },
52
+ {
53
+ input: {
54
+ permissions: {
55
+ user: {
56
+ where: {
57
+ id: "1"
58
+ },
59
+ select: true,
60
+ update: true,
61
+ }
62
+ }
63
+ },
64
+ output: {
65
+ insert_permissions: [],
66
+ select_permissions: [{
67
+ role: "user",
68
+ permission: {
69
+ columns: ["id"],
70
+ filter: {
71
+ id: { _eq: "1" }
72
+ }
73
+ }
74
+ }],
75
+ update_permissions: [{
76
+ role: "user",
77
+ permission: {
78
+ columns: ["id"],
79
+ filter: {
80
+ id: { _eq: "1" }
81
+ }
82
+ }
83
+ }],
84
+ delete_permissions: [],
85
+ }
86
+ },
87
+ {
88
+ input: {
89
+ permissions: {
90
+ user: {
91
+ where: {
92
+ id: "1"
93
+ },
94
+ select: true,
95
+ update: {
96
+ where: {
97
+ users: {
98
+ id: "1"
99
+ }
100
+ },
101
+ },
102
+ }
103
+ }
104
+ },
105
+ output: {
106
+ insert_permissions: [],
107
+ select_permissions: [{
108
+ role: "user",
109
+ permission: {
110
+ columns: ["id"],
111
+ filter: {
112
+ id: { _eq: "1" }
113
+ }
114
+ }
115
+ }],
116
+ update_permissions: [{
117
+ role: "user",
118
+ permission: {
119
+ columns: ["id"],
120
+ filter: {
121
+ _and: [
122
+ { id: { _eq: "1" } },
123
+ {
124
+ users: {
125
+ id: { _eq: "1" }
126
+ }
127
+ }
128
+ ]
129
+ }
130
+ }
131
+ }],
132
+ delete_permissions: [],
133
+ }
134
+ }
135
+ ]
136
+
137
+ describe("convert whereTypeorm to hasuraObj", () => {
138
+ cases.forEach(({ input, output }) =>
139
+ it("input to Equal output", () =>
140
+ expect(generatePermissions(dataSourceOptions, input, TestColumns))
141
+ .toEqual(output))
142
+ )
143
+ })
@@ -0,0 +1,84 @@
1
+ import type * as Hasura from "hasura-metadata-types";
2
+ import { ColumnMetadata, DataSourceOptions, EntityOptions, UserActionType } from "../types";
3
+ import { convertWhereClause } from "./whereClause"
4
+ export type PermissionResult = Required<Pick<
5
+ Hasura.MetadataTable,
6
+ "insert_permissions" | "select_permissions" | "update_permissions" | "delete_permissions"
7
+ >>
8
+
9
+ export function generatePermissions<Entity extends Object = Object>(
10
+ dataSourceOptions: DataSourceOptions,
11
+ entityOptions: EntityOptions<Entity> | undefined,
12
+ columnMetadata: ColumnMetadata[]
13
+ ): PermissionResult {
14
+
15
+ const result: PermissionResult = {
16
+ insert_permissions: [],
17
+ select_permissions: [],
18
+ update_permissions: [],
19
+ delete_permissions: [],
20
+ }
21
+ if (!entityOptions?.permissions)
22
+ return result;
23
+
24
+ const { permissions } = entityOptions
25
+
26
+ for (let key in permissions) {
27
+ const permission = permissions[key]
28
+ let { where, select, update, insert, delete: myDelete } = permission
29
+
30
+ if (select) {
31
+ if (select === true) select = {}
32
+ result.select_permissions.push({
33
+ role: key,
34
+ permission: {
35
+ columns: columnNames(columnMetadata, key, "select"),
36
+ filter: convertWhereClause<Entity>(where, select.where),
37
+ limit: select.limit || dataSourceOptions.defaultSelectPermissionLimit,
38
+ }
39
+ })
40
+ }
41
+ if (update) {
42
+ if (update === true) update = {}
43
+ result.update_permissions.push({
44
+ role: key,
45
+ permission: {
46
+ columns: columnNames(columnMetadata, key, "update"),
47
+ filter: convertWhereClause<Entity>(where, update.where)
48
+ }
49
+ })
50
+ }
51
+
52
+ if (insert) {
53
+ if (insert === true) insert = {}
54
+ if (insert.where) throw new Error("u can not select while inserting something")
55
+ result.insert_permissions.push({
56
+ role: key,
57
+ permission: {
58
+ columns: columnNames(columnMetadata, key, "insert"),
59
+ }
60
+ })
61
+ }
62
+
63
+ if (myDelete) {
64
+ if (myDelete === true) myDelete = {}
65
+ result.delete_permissions.push({
66
+ role: key,
67
+ permission: {
68
+ filter: convertWhereClause<Entity>(where, myDelete.where)
69
+ }
70
+ })
71
+ }
72
+ }
73
+ return result
74
+ }
75
+
76
+ function columnNames(arr: ColumnMetadata[], roleName: string, method: UserActionType): string[] {
77
+ return arr.filter(column => {
78
+ let permission = column.options?.permissions?.[roleName]
79
+ if (typeof permission == "boolean" || permission == undefined) return false;
80
+ if (typeof permission == "string")
81
+ permission = [permission]
82
+ return permission.includes(method)
83
+ }).map(i => i.propertyName)
84
+ }