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.
- package/.env.example +9 -0
- package/LICENSE +21 -0
- package/dev-playground/UserRole.ts +5 -0
- package/dev-playground/action/currencyConverter.ts +60 -0
- package/dev-playground/data-source.ts +21 -0
- package/dev-playground/entity/Org.ts +52 -0
- package/dev-playground/entity/Product.ts +74 -0
- package/dev-playground/entity/User.ts +51 -0
- package/dev-playground/entity/index.ts +3 -0
- package/dev-playground/hasura.ts +35 -0
- package/dev-playground/index.ts +43 -0
- package/dev-playground/migration/1679166386871-next.ts +29 -0
- package/jest.config.js +6 -0
- package/package.json +70 -0
- package/random-notes.md +8 -0
- package/rollup.config.mjs +56 -0
- package/src/builders/Action.ts +27 -0
- package/src/builders/Metadata.ts +96 -0
- package/src/builders/index.ts +2 -0
- package/src/decorators/Column.ts +12 -0
- package/src/decorators/Entity.ts +12 -0
- package/src/decorators/index.ts +2 -0
- package/src/index.ts +5 -0
- package/src/internalStorage.ts +23 -0
- package/src/mappers/databaseUrl.spec.ts +33 -0
- package/src/mappers/databaseUrl.ts +19 -0
- package/src/mappers/graphql.spec.ts +124 -0
- package/src/mappers/graphql.ts +61 -0
- package/src/mappers/hasuraKind.spec.ts +12 -0
- package/src/mappers/hasuraKind.ts +11 -0
- package/src/mappers/index.ts +3 -0
- package/src/mappers/permissions.spec.ts +143 -0
- package/src/mappers/permissions.ts +84 -0
- package/src/mappers/relationships.ts +65 -0
- package/src/mappers/source.ts +29 -0
- package/src/mappers/table.ts +27 -0
- package/src/mappers/tableConfiguration.ts +45 -0
- package/src/mappers/whereClause.spec.ts +85 -0
- package/src/mappers/whereClause.ts +41 -0
- package/src/types/Action.ts +27 -0
- package/src/types/Column.ts +23 -0
- package/src/types/DataSourceOptions.ts +22 -0
- package/src/types/Entity.ts +53 -0
- package/src/types/base.ts +2 -0
- package/src/types/index.ts +7 -0
- package/src/types/permissions.ts +15 -0
- package/src/types/whereClause.ts +75 -0
- 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,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
|
+
}
|
package/src/index.ts
ADDED
|
@@ -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,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
|
+
}
|