Skip to content

Commit

Permalink
feat(api-form-builder): storage operations (#1942)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunozoric committed Oct 11, 2021
1 parent 79027c6 commit a041a2c
Show file tree
Hide file tree
Showing 145 changed files with 6,862 additions and 2,639 deletions.
1 change: 1 addition & 0 deletions api/code/graphql/package.json
Expand Up @@ -11,6 +11,7 @@
"@webiny/api-file-manager-ddb-es": "^5.15.0",
"@webiny/api-file-manager-s3": "^5.15.0",
"@webiny/api-form-builder": "^5.15.0",
"@webiny/api-form-builder-so-ddb-es": "^5.15.0",
"@webiny/api-headless-cms": "^5.15.0",
"@webiny/api-headless-cms-ddb-es": "^5.15.0",
"@webiny/api-i18n": "^5.15.0",
Expand Down
27 changes: 20 additions & 7 deletions api/code/graphql/src/index.ts
Expand Up @@ -18,30 +18,38 @@ import fileManagerPlugins from "@webiny/api-file-manager/plugins";
import fileManagerDynamoDbElasticStorageOperation from "@webiny/api-file-manager-ddb-es";
import logsPlugins from "@webiny/handler-logs";
import fileManagerS3 from "@webiny/api-file-manager-s3";
import formBuilderPlugins from "@webiny/api-form-builder/plugins";
import { createFormBuilder } from "@webiny/api-form-builder";
import securityPlugins from "./security";
import headlessCmsPlugins from "@webiny/api-headless-cms/plugins";
import headlessCmsDynamoDbElasticStorageOperation from "@webiny/api-headless-cms-ddb-es";
import elasticsearchDataGzipCompression from "@webiny/api-elasticsearch/plugins/GzipCompression";
import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es";

// Imports plugins created via scaffolding utilities.
import scaffoldsPlugins from "./plugins/scaffolds";
import { createElasticsearchClient } from "@webiny/api-elasticsearch/client";

const debug = process.env.DEBUG === "true";

const documentClient = new DocumentClient({
convertEmptyValues: true,
region: process.env.AWS_REGION
});

const elasticsearchClient = createElasticsearchClient({
endpoint: `https://${process.env.ELASTIC_SEARCH_ENDPOINT}`
});

export const handler = createHandler({
plugins: [
dynamoDbPlugins(),
logsPlugins(),
graphqlPlugins({ debug }),
elasticSearch({ endpoint: `https://${process.env.ELASTIC_SEARCH_ENDPOINT}` }),
elasticSearch(elasticsearchClient),
dbPlugins({
table: process.env.DB_TABLE,
driver: new DynamoDbDriver({
documentClient: new DocumentClient({
convertEmptyValues: true,
region: process.env.AWS_REGION
})
documentClient
})
}),
securityPlugins(),
Expand All @@ -67,7 +75,12 @@ export const handler = createHandler({
pageBuilderPlugins(),
pageBuilderDynamoDbElasticsearchPlugins(),
pageBuilderPrerenderingPlugins(),
formBuilderPlugins(),
createFormBuilder({
storageOperations: createFormBuilderStorageOperations({
documentClient,
elasticsearch: elasticsearchClient
})
}),
headlessCmsPlugins(),
headlessCmsDynamoDbElasticStorageOperation(),
scaffoldsPlugins(),
Expand Down
7 changes: 7 additions & 0 deletions api/code/graphql/tsconfig.json
Expand Up @@ -53,6 +53,9 @@
{
"path": "../../../packages/api-form-builder"
},
{
"path": "../../../packages/api-form-builder-so-ddb-es"
},
{
"path": "../../../packages/api-elasticsearch"
},
Expand Down Expand Up @@ -116,6 +119,10 @@
"@webiny/api-page-builder": ["../../../packages/api-page-builder/src"],
"@webiny/api-form-builder/*": ["../../../packages/api-form-builder/src/*"],
"@webiny/api-form-builder": ["../../../packages/api-form-builder/src"],
"@webiny/api-form-builder-so-ddb-es/*": [
"../../../packages/api-form-builder-so-ddb-es/src/*"
],
"@webiny/api-form-builder-so-ddb-es": ["../../../packages/api-form-builder-so-ddb-es/src"],
"@webiny/api-security/*": ["../../../packages/api-security/src/*"],
"@webiny/api-security": ["../../../packages/api-security/src"],
"@webiny/api-tenancy/*": ["../../../packages/api-tenancy/src/*"],
Expand Down
54 changes: 54 additions & 0 deletions packages/api-elasticsearch/__tests__/where.test.ts
@@ -0,0 +1,54 @@
import { parseWhereKey } from "~/where";

describe("where", () => {
const whereKeys = [
[
"id",
{
field: "id",
operator: "eq"
}
],
[
"id_in",
{
field: "id",
operator: "in"
}
],
[
"id_not_in",
{
field: "id",
operator: "not_in"
}
]
];

test.each(whereKeys)(
"parse should result in field and operator values",
(key: string, expected: any) => {
const result = parseWhereKey(key);

expect(result).toEqual(expected);
}
);

const malformedWhereKeys = [["_a"], ["_"], ["__"], ["a_"]];

test.each(malformedWhereKeys)(
`should throw error when malformed key is passed "%s"`,
(key: string) => {
expect(() => {
parseWhereKey(key);
}).toThrow(`It is not possible to search by key "${key}"`);
}
);

test("should throw error when malformed field is parsed out", () => {
const key = "a0_in";
expect(() => {
parseWhereKey(key);
}).toThrow(`Cannot filter by "a0".`);
});
});
25 changes: 25 additions & 0 deletions packages/api-elasticsearch/src/client.ts
@@ -0,0 +1,25 @@
import { Client, ClientOptions } from "@elastic/elasticsearch";
import AWS from "aws-sdk";
import createAwsElasticsearchConnector from "aws-elasticsearch-connector";

export interface ElasticsearchClientOptions extends ClientOptions {
endpoint?: string;
}

export const createElasticsearchClient = (options: ElasticsearchClientOptions) => {
const { endpoint, node, ...rest } = options;

const clientOptions: ClientOptions = {
node: endpoint || node,
...rest
};

if (!clientOptions.auth) {
/**
* If no `auth` configuration is present, we setup AWS connector.
*/
Object.assign(clientOptions, createAwsElasticsearchConnector(AWS.config));
}

return new Client(clientOptions);
};
63 changes: 12 additions & 51 deletions packages/api-elasticsearch/src/index.ts
@@ -1,68 +1,29 @@
import { Client, ClientOptions } from "@elastic/elasticsearch";
import AWS from "aws-sdk";
import createAwsElasticsearchConnector from "aws-elasticsearch-connector";
import { ElasticsearchContext } from "~/types";
import { ContextPlugin } from "@webiny/handler/plugins/ContextPlugin";
import {
ElasticsearchQueryBuilderOperatorBetweenPlugin,
ElasticsearchQueryBuilderOperatorNotBetweenPlugin,
ElasticsearchQueryBuilderOperatorContainsPlugin,
ElasticsearchQueryBuilderOperatorNotContainsPlugin,
ElasticsearchQueryBuilderOperatorEqualPlugin,
ElasticsearchQueryBuilderOperatorNotPlugin,
ElasticsearchQueryBuilderOperatorGreaterThanPlugin,
ElasticsearchQueryBuilderOperatorGreaterThanOrEqualToPlugin,
ElasticsearchQueryBuilderOperatorLesserThanPlugin,
ElasticsearchQueryBuilderOperatorLesserThanOrEqualToPlugin,
ElasticsearchQueryBuilderOperatorInPlugin,
ElasticsearchQueryBuilderOperatorAndInPlugin,
ElasticsearchQueryBuilderOperatorNotInPlugin
} from "~/plugins/operator";
import WebinyError from "@webiny/error";
import { createElasticsearchClient, ElasticsearchClientOptions } from "~/client";
import { getElasticsearchOperators } from "~/operators";
import { Client } from "@elastic/elasticsearch";

interface ElasticsearchClientOptions extends ClientOptions {
endpoint?: string;
}

export default (options: ElasticsearchClientOptions): ContextPlugin<ElasticsearchContext> => {
const { endpoint, node, ...rest } = options;
/**
* We must accept either Elasticsearch client or options that create the client.
*/
export default (
params: ElasticsearchClientOptions | Client
): ContextPlugin<ElasticsearchContext> => {
return new ContextPlugin<ElasticsearchContext>(context => {
if (context.elasticsearch) {
throw new WebinyError(
"Elasticsearch client is already initialized, no need to define it again. Check your code for duplicate initializations.",
"ELASTICSEARCH_ALREADY_INITIALIZED"
);
}
const clientOptions: ClientOptions = {
node: endpoint || node,
...rest
};

if (!clientOptions.auth) {
/**
* If no `auth` configuration is present, we setup AWS connector.
*/
Object.assign(clientOptions, createAwsElasticsearchConnector(AWS.config));
}
/**
* Initialize the Elasticsearch client.
*/
context.elasticsearch = new Client(clientOptions);
context.elasticsearch =
params instanceof Client ? params : createElasticsearchClient(params);

context.plugins.register([
new ElasticsearchQueryBuilderOperatorBetweenPlugin(),
new ElasticsearchQueryBuilderOperatorNotBetweenPlugin(),
new ElasticsearchQueryBuilderOperatorContainsPlugin(),
new ElasticsearchQueryBuilderOperatorNotContainsPlugin(),
new ElasticsearchQueryBuilderOperatorEqualPlugin(),
new ElasticsearchQueryBuilderOperatorNotPlugin(),
new ElasticsearchQueryBuilderOperatorGreaterThanPlugin(),
new ElasticsearchQueryBuilderOperatorGreaterThanOrEqualToPlugin(),
new ElasticsearchQueryBuilderOperatorLesserThanPlugin(),
new ElasticsearchQueryBuilderOperatorLesserThanOrEqualToPlugin(),
new ElasticsearchQueryBuilderOperatorInPlugin(),
new ElasticsearchQueryBuilderOperatorAndInPlugin(),
new ElasticsearchQueryBuilderOperatorNotInPlugin()
]);
context.plugins.register(getElasticsearchOperators());
});
};
34 changes: 34 additions & 0 deletions packages/api-elasticsearch/src/operators.ts
@@ -0,0 +1,34 @@
import { ElasticsearchQueryBuilderOperatorBetweenPlugin } from "~/plugins/operator/between";
import { ElasticsearchQueryBuilderOperatorNotBetweenPlugin } from "~/plugins/operator/notBetween";
import { ElasticsearchQueryBuilderOperatorContainsPlugin } from "~/plugins/operator/contains";
import { ElasticsearchQueryBuilderOperatorNotContainsPlugin } from "~/plugins/operator/notContains";
import { ElasticsearchQueryBuilderOperatorEqualPlugin } from "~/plugins/operator/equal";
import { ElasticsearchQueryBuilderOperatorNotPlugin } from "~/plugins/operator/not";
import { ElasticsearchQueryBuilderOperatorGreaterThanPlugin } from "~/plugins/operator/gt";
import { ElasticsearchQueryBuilderOperatorGreaterThanOrEqualToPlugin } from "~/plugins/operator/gte";
import { ElasticsearchQueryBuilderOperatorLesserThanPlugin } from "~/plugins/operator/lt";
import { ElasticsearchQueryBuilderOperatorLesserThanOrEqualToPlugin } from "~/plugins/operator/lte";
import { ElasticsearchQueryBuilderOperatorInPlugin } from "~/plugins/operator/in";
import { ElasticsearchQueryBuilderOperatorAndInPlugin } from "~/plugins/operator/andIn";
import { ElasticsearchQueryBuilderOperatorNotInPlugin } from "~/plugins/operator/notIn";

const operators = [
new ElasticsearchQueryBuilderOperatorBetweenPlugin(),
new ElasticsearchQueryBuilderOperatorNotBetweenPlugin(),
new ElasticsearchQueryBuilderOperatorContainsPlugin(),
new ElasticsearchQueryBuilderOperatorNotContainsPlugin(),
new ElasticsearchQueryBuilderOperatorEqualPlugin(),
new ElasticsearchQueryBuilderOperatorNotPlugin(),
new ElasticsearchQueryBuilderOperatorGreaterThanPlugin(),
new ElasticsearchQueryBuilderOperatorGreaterThanOrEqualToPlugin(),
new ElasticsearchQueryBuilderOperatorLesserThanPlugin(),
new ElasticsearchQueryBuilderOperatorLesserThanOrEqualToPlugin(),
new ElasticsearchQueryBuilderOperatorInPlugin(),
new ElasticsearchQueryBuilderOperatorAndInPlugin(),
new ElasticsearchQueryBuilderOperatorNotInPlugin()
];
/**
* We export as a function because there might be something to be sent to the operators at some point.
* This way, we make it easier to upgrade.
*/
export const getElasticsearchOperators = () => operators;
@@ -1,28 +1,24 @@
import WebinyError from "@webiny/error";
import { Plugin } from "@webiny/plugins";
import { ContextInterface } from "@webiny/handler/types";
import { SearchBody } from "elastic-ts";

export interface ModifyBodyParams<T extends ContextInterface> {
context: T;
export interface ModifyBodyParams {
body: SearchBody;
}

interface Callable<T extends ContextInterface> {
(params: ModifyBodyParams<T>): void;
interface Callable {
(params: ModifyBodyParams): void;
}

export abstract class ElasticsearchBodyModifierPlugin<
T extends ContextInterface = ContextInterface
> extends Plugin {
private readonly callable?: Callable<T>;
export abstract class ElasticsearchBodyModifierPlugin extends Plugin {
private readonly callable?: Callable;

public constructor(callable?: Callable<T>) {
public constructor(callable?: Callable) {
super();
this.callable = callable;
}

public modifyBody(params: ModifyBodyParams<T>): void {
public modifyBody(params: ModifyBodyParams): void {
if (typeof this.callable !== "function") {
throw new WebinyError(
`Missing modification for the body.`,
Expand Down
@@ -1,14 +1,18 @@
import { Plugin } from "@webiny/plugins";
import { FieldSortOptions, SortOrder } from "elastic-ts";
import { ContextInterface } from "@webiny/handler/types";

export type UnmappedTypes = "date" | "long" | string;

const keywordLessUnmappedType = ["date", "long"];

const unmappedTypeHasKeyword = (type: string): boolean => {
if (keywordLessUnmappedType.includes(type)) {
return false;
}
return true;
};

export interface ToSearchValueParams {
/**
* Some variable that has a ContextInterface as a base.
*/
context: ContextInterface;
/**
* The value to transform.
*/
Expand Down Expand Up @@ -96,6 +100,9 @@ export abstract class ElasticsearchFieldPlugin extends Plugin {
this._path = params.path || params.field;
this._keyword = params.keyword === undefined ? true : params.keyword;
this._unmappedType = params.unmappedType;
if (unmappedTypeHasKeyword(params.unmappedType) === false) {
this._keyword = false;
}
this._sortable = params.sortable === undefined ? true : params.sortable;
this._searchable = params.searchable === undefined ? true : params.searchable;
}
Expand Down
@@ -1,29 +1,25 @@
import WebinyError from "@webiny/error";
import { Plugin } from "@webiny/plugins";
import { ElasticsearchBoolQueryConfig } from "~/types";
import { ContextInterface } from "@webiny/handler/types";

export interface ModifyQueryParams<T extends ContextInterface> {
context: T;
export interface ModifyQueryParams {
query: ElasticsearchBoolQueryConfig;
where: Record<string, any>;
}

interface Callable<T extends ContextInterface> {
(params: ModifyQueryParams<T>): void;
interface Callable {
(params: ModifyQueryParams): void;
}

export abstract class ElasticsearchQueryModifierPlugin<
T extends ContextInterface = ContextInterface
> extends Plugin {
private readonly callable?: Callable<T>;
export abstract class ElasticsearchQueryModifierPlugin extends Plugin {
private readonly callable?: Callable;

public constructor(callable?: Callable<T>) {
public constructor(callable?: Callable) {
super();
this.callable = callable;
}

public modifyQuery(params: ModifyQueryParams<T>): void {
public modifyQuery(params: ModifyQueryParams): void {
if (typeof this.callable !== "function") {
throw new WebinyError(
`Missing modification for the query.`,
Expand Down

0 comments on commit a041a2c

Please sign in to comment.