@snowtop/ent 0.1.0-alpha160-test5 → 0.1.0-alpha160-test6
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/dist/package.json +64 -0
- package/{scripts → dist/scripts}/custom_compiler.js +0 -0
- package/{scripts → dist/scripts}/custom_graphql.js +0 -0
- package/package.json +47 -5
- package/src/action/action.ts +330 -0
- package/src/action/executor.ts +453 -0
- package/src/action/experimental_action.ts +277 -0
- package/src/action/index.ts +31 -0
- package/src/action/operations.ts +967 -0
- package/src/action/orchestrator.ts +1527 -0
- package/src/action/privacy.ts +37 -0
- package/src/action/relative_value.ts +242 -0
- package/src/action/transaction.ts +38 -0
- package/src/auth/auth.ts +77 -0
- package/src/auth/index.ts +8 -0
- package/src/core/base.ts +367 -0
- package/src/core/clause.ts +1065 -0
- package/src/core/config.ts +219 -0
- package/src/core/const.ts +5 -0
- package/src/core/context.ts +135 -0
- package/src/core/convert.ts +106 -0
- package/src/core/date.ts +23 -0
- package/src/core/db.ts +498 -0
- package/src/core/ent.ts +1740 -0
- package/src/core/global_schema.ts +49 -0
- package/src/core/loaders/assoc_count_loader.ts +99 -0
- package/src/core/loaders/assoc_edge_loader.ts +250 -0
- package/src/core/loaders/index.ts +12 -0
- package/src/core/loaders/loader.ts +66 -0
- package/src/core/loaders/object_loader.ts +489 -0
- package/src/core/loaders/query_loader.ts +314 -0
- package/src/core/loaders/raw_count_loader.ts +175 -0
- package/src/core/logger.ts +49 -0
- package/src/core/privacy.ts +660 -0
- package/src/core/query/assoc_query.ts +240 -0
- package/src/core/query/custom_clause_query.ts +174 -0
- package/src/core/query/custom_query.ts +302 -0
- package/src/core/query/index.ts +9 -0
- package/src/core/query/query.ts +674 -0
- package/src/core/query_impl.ts +32 -0
- package/src/core/viewer.ts +52 -0
- package/src/ent.code-workspace +73 -0
- package/src/graphql/builtins/connection.ts +25 -0
- package/src/graphql/builtins/edge.ts +16 -0
- package/src/graphql/builtins/node.ts +12 -0
- package/src/graphql/graphql.ts +891 -0
- package/src/graphql/graphql_field_helpers.ts +221 -0
- package/src/graphql/index.ts +42 -0
- package/src/graphql/mutations/union.ts +39 -0
- package/src/graphql/node_resolver.ts +122 -0
- package/src/graphql/query/connection_type.ts +113 -0
- package/src/graphql/query/edge_connection.ts +171 -0
- package/src/graphql/query/page_info.ts +34 -0
- package/src/graphql/query/shared_edge_connection.ts +287 -0
- package/src/graphql/scalars/orderby_direction.ts +13 -0
- package/src/graphql/scalars/time.ts +38 -0
- package/src/imports/dataz/example1/_auth.ts +51 -0
- package/src/imports/dataz/example1/_viewer.ts +35 -0
- package/src/imports/index.ts +213 -0
- package/src/index.ts +145 -0
- package/src/parse_schema/parse.ts +585 -0
- package/src/schema/base_schema.ts +224 -0
- package/src/schema/field.ts +1087 -0
- package/src/schema/index.ts +53 -0
- package/src/schema/json_field.ts +94 -0
- package/src/schema/schema.ts +1028 -0
- package/src/schema/struct_field.ts +234 -0
- package/src/schema/union_field.ts +105 -0
- package/src/scripts/custom_compiler.ts +331 -0
- package/src/scripts/custom_graphql.ts +550 -0
- package/src/scripts/migrate_v0.1.ts +41 -0
- package/src/scripts/move_types.ts +131 -0
- package/src/scripts/read_schema.ts +67 -0
- package/src/setupPackage.js +42 -0
- package/src/testutils/action/complex_schemas.ts +517 -0
- package/src/testutils/builder.ts +422 -0
- package/src/testutils/context/test_context.ts +25 -0
- package/src/testutils/db/fixture.ts +32 -0
- package/src/testutils/db/temp_db.ts +941 -0
- package/src/testutils/db/value.ts +294 -0
- package/src/testutils/db_mock.ts +351 -0
- package/src/testutils/db_time_zone.ts +40 -0
- package/src/testutils/ent-graphql-tests/index.ts +653 -0
- package/src/testutils/fake_comms.ts +50 -0
- package/src/testutils/fake_data/const.ts +64 -0
- package/src/testutils/fake_data/events_query.ts +145 -0
- package/src/testutils/fake_data/fake_contact.ts +150 -0
- package/src/testutils/fake_data/fake_event.ts +150 -0
- package/src/testutils/fake_data/fake_tag.ts +139 -0
- package/src/testutils/fake_data/fake_user.ts +232 -0
- package/src/testutils/fake_data/index.ts +1 -0
- package/src/testutils/fake_data/internal.ts +8 -0
- package/src/testutils/fake_data/tag_query.ts +56 -0
- package/src/testutils/fake_data/test_helpers.ts +388 -0
- package/src/testutils/fake_data/user_query.ts +524 -0
- package/src/testutils/fake_log.ts +52 -0
- package/src/testutils/mock_date.ts +10 -0
- package/src/testutils/mock_log.ts +39 -0
- package/src/testutils/parse_sql.ts +685 -0
- package/src/testutils/test_edge_global_schema.ts +49 -0
- package/src/testutils/write.ts +70 -0
- package/src/tsc/ast.ts +351 -0
- package/src/tsc/compilerOptions.ts +85 -0
- package/src/tsc/move_generated.ts +191 -0
- package/src/tsc/transform.ts +226 -0
- package/src/tsc/transform_action.ts +224 -0
- package/src/tsc/transform_ent.ts +66 -0
- package/src/tsc/transform_schema.ts +546 -0
- package/tsconfig.json +20 -0
- package/core/query/shared_assoc_test.d.ts +0 -2
- package/core/query/shared_assoc_test.js +0 -804
- package/core/query/shared_test.d.ts +0 -21
- package/core/query/shared_test.js +0 -736
- package/graphql/query/shared_assoc_test.d.ts +0 -1
- package/graphql/query/shared_assoc_test.js +0 -203
- /package/{action → dist/action}/action.d.ts +0 -0
- /package/{action → dist/action}/action.js +0 -0
- /package/{action → dist/action}/executor.d.ts +0 -0
- /package/{action → dist/action}/executor.js +0 -0
- /package/{action → dist/action}/experimental_action.d.ts +0 -0
- /package/{action → dist/action}/experimental_action.js +0 -0
- /package/{action → dist/action}/index.d.ts +0 -0
- /package/{action → dist/action}/index.js +0 -0
- /package/{action → dist/action}/operations.d.ts +0 -0
- /package/{action → dist/action}/operations.js +0 -0
- /package/{action → dist/action}/orchestrator.d.ts +0 -0
- /package/{action → dist/action}/orchestrator.js +0 -0
- /package/{action → dist/action}/privacy.d.ts +0 -0
- /package/{action → dist/action}/privacy.js +0 -0
- /package/{action → dist/action}/relative_value.d.ts +0 -0
- /package/{action → dist/action}/relative_value.js +0 -0
- /package/{action → dist/action}/transaction.d.ts +0 -0
- /package/{action → dist/action}/transaction.js +0 -0
- /package/{auth → dist/auth}/auth.d.ts +0 -0
- /package/{auth → dist/auth}/auth.js +0 -0
- /package/{auth → dist/auth}/index.d.ts +0 -0
- /package/{auth → dist/auth}/index.js +0 -0
- /package/{core → dist/core}/base.d.ts +0 -0
- /package/{core → dist/core}/base.js +0 -0
- /package/{core → dist/core}/clause.d.ts +0 -0
- /package/{core → dist/core}/clause.js +0 -0
- /package/{core → dist/core}/config.d.ts +0 -0
- /package/{core → dist/core}/config.js +0 -0
- /package/{core → dist/core}/const.d.ts +0 -0
- /package/{core → dist/core}/const.js +0 -0
- /package/{core → dist/core}/context.d.ts +0 -0
- /package/{core → dist/core}/context.js +0 -0
- /package/{core → dist/core}/convert.d.ts +0 -0
- /package/{core → dist/core}/convert.js +0 -0
- /package/{core → dist/core}/date.d.ts +0 -0
- /package/{core → dist/core}/date.js +0 -0
- /package/{core → dist/core}/db.d.ts +0 -0
- /package/{core → dist/core}/db.js +0 -0
- /package/{core → dist/core}/ent.d.ts +0 -0
- /package/{core → dist/core}/ent.js +0 -0
- /package/{core → dist/core}/global_schema.d.ts +0 -0
- /package/{core → dist/core}/global_schema.js +0 -0
- /package/{core → dist/core}/loaders/assoc_count_loader.d.ts +0 -0
- /package/{core → dist/core}/loaders/assoc_count_loader.js +0 -0
- /package/{core → dist/core}/loaders/assoc_edge_loader.d.ts +0 -0
- /package/{core → dist/core}/loaders/assoc_edge_loader.js +0 -0
- /package/{core → dist/core}/loaders/index.d.ts +0 -0
- /package/{core → dist/core}/loaders/index.js +0 -0
- /package/{core → dist/core}/loaders/loader.d.ts +0 -0
- /package/{core → dist/core}/loaders/loader.js +0 -0
- /package/{core → dist/core}/loaders/object_loader.d.ts +0 -0
- /package/{core → dist/core}/loaders/object_loader.js +0 -0
- /package/{core → dist/core}/loaders/query_loader.d.ts +0 -0
- /package/{core → dist/core}/loaders/query_loader.js +0 -0
- /package/{core → dist/core}/loaders/raw_count_loader.d.ts +0 -0
- /package/{core → dist/core}/loaders/raw_count_loader.js +0 -0
- /package/{core → dist/core}/logger.d.ts +0 -0
- /package/{core → dist/core}/logger.js +0 -0
- /package/{core → dist/core}/privacy.d.ts +0 -0
- /package/{core → dist/core}/privacy.js +0 -0
- /package/{core → dist/core}/query/assoc_query.d.ts +0 -0
- /package/{core → dist/core}/query/assoc_query.js +0 -0
- /package/{core → dist/core}/query/custom_clause_query.d.ts +0 -0
- /package/{core → dist/core}/query/custom_clause_query.js +0 -0
- /package/{core → dist/core}/query/custom_query.d.ts +0 -0
- /package/{core → dist/core}/query/custom_query.js +0 -0
- /package/{core → dist/core}/query/index.d.ts +0 -0
- /package/{core → dist/core}/query/index.js +0 -0
- /package/{core → dist/core}/query/query.d.ts +0 -0
- /package/{core → dist/core}/query/query.js +0 -0
- /package/{core → dist/core}/query_impl.d.ts +0 -0
- /package/{core → dist/core}/query_impl.js +0 -0
- /package/{core → dist/core}/viewer.d.ts +0 -0
- /package/{core → dist/core}/viewer.js +0 -0
- /package/{graphql → dist/graphql}/builtins/connection.d.ts +0 -0
- /package/{graphql → dist/graphql}/builtins/connection.js +0 -0
- /package/{graphql → dist/graphql}/builtins/edge.d.ts +0 -0
- /package/{graphql → dist/graphql}/builtins/edge.js +0 -0
- /package/{graphql → dist/graphql}/builtins/node.d.ts +0 -0
- /package/{graphql → dist/graphql}/builtins/node.js +0 -0
- /package/{graphql → dist/graphql}/graphql.d.ts +0 -0
- /package/{graphql → dist/graphql}/graphql.js +0 -0
- /package/{graphql → dist/graphql}/graphql_field_helpers.d.ts +0 -0
- /package/{graphql → dist/graphql}/graphql_field_helpers.js +0 -0
- /package/{graphql → dist/graphql}/index.d.ts +0 -0
- /package/{graphql → dist/graphql}/index.js +0 -0
- /package/{graphql → dist/graphql}/mutations/union.d.ts +0 -0
- /package/{graphql → dist/graphql}/mutations/union.js +0 -0
- /package/{graphql → dist/graphql}/node_resolver.d.ts +0 -0
- /package/{graphql → dist/graphql}/node_resolver.js +0 -0
- /package/{graphql → dist/graphql}/query/connection_type.d.ts +0 -0
- /package/{graphql → dist/graphql}/query/connection_type.js +0 -0
- /package/{graphql → dist/graphql}/query/edge_connection.d.ts +0 -0
- /package/{graphql → dist/graphql}/query/edge_connection.js +0 -0
- /package/{graphql → dist/graphql}/query/page_info.d.ts +0 -0
- /package/{graphql → dist/graphql}/query/page_info.js +0 -0
- /package/{graphql → dist/graphql}/query/shared_edge_connection.d.ts +0 -0
- /package/{graphql → dist/graphql}/query/shared_edge_connection.js +0 -0
- /package/{graphql → dist/graphql}/scalars/orderby_direction.d.ts +0 -0
- /package/{graphql → dist/graphql}/scalars/orderby_direction.js +0 -0
- /package/{graphql → dist/graphql}/scalars/time.d.ts +0 -0
- /package/{graphql → dist/graphql}/scalars/time.js +0 -0
- /package/{imports → dist/imports}/dataz/example1/_auth.d.ts +0 -0
- /package/{imports → dist/imports}/dataz/example1/_auth.js +0 -0
- /package/{imports → dist/imports}/dataz/example1/_viewer.d.ts +0 -0
- /package/{imports → dist/imports}/dataz/example1/_viewer.js +0 -0
- /package/{imports → dist/imports}/index.d.ts +0 -0
- /package/{imports → dist/imports}/index.js +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{index.js → dist/index.js} +0 -0
- /package/{parse_schema → dist/parse_schema}/parse.d.ts +0 -0
- /package/{parse_schema → dist/parse_schema}/parse.js +0 -0
- /package/{schema → dist/schema}/base_schema.d.ts +0 -0
- /package/{schema → dist/schema}/base_schema.js +0 -0
- /package/{schema → dist/schema}/field.d.ts +0 -0
- /package/{schema → dist/schema}/field.js +0 -0
- /package/{schema → dist/schema}/index.d.ts +0 -0
- /package/{schema → dist/schema}/index.js +0 -0
- /package/{schema → dist/schema}/json_field.d.ts +0 -0
- /package/{schema → dist/schema}/json_field.js +0 -0
- /package/{schema → dist/schema}/schema.d.ts +0 -0
- /package/{schema → dist/schema}/schema.js +0 -0
- /package/{schema → dist/schema}/struct_field.d.ts +0 -0
- /package/{schema → dist/schema}/struct_field.js +0 -0
- /package/{schema → dist/schema}/union_field.d.ts +0 -0
- /package/{schema → dist/schema}/union_field.js +0 -0
- /package/{scripts → dist/scripts}/custom_compiler.d.ts +0 -0
- /package/{scripts → dist/scripts}/custom_graphql.d.ts +0 -0
- /package/{scripts → dist/scripts}/migrate_v0.1.d.ts +0 -0
- /package/{scripts → dist/scripts}/migrate_v0.1.js +0 -0
- /package/{scripts → dist/scripts}/move_types.d.ts +0 -0
- /package/{scripts → dist/scripts}/move_types.js +0 -0
- /package/{scripts → dist/scripts}/read_schema.d.ts +0 -0
- /package/{scripts → dist/scripts}/read_schema.js +0 -0
- /package/{testutils → dist/testutils}/action/complex_schemas.d.ts +0 -0
- /package/{testutils → dist/testutils}/action/complex_schemas.js +0 -0
- /package/{testutils → dist/testutils}/builder.d.ts +0 -0
- /package/{testutils → dist/testutils}/builder.js +0 -0
- /package/{testutils → dist/testutils}/context/test_context.d.ts +0 -0
- /package/{testutils → dist/testutils}/context/test_context.js +0 -0
- /package/{testutils → dist/testutils}/db/fixture.d.ts +0 -0
- /package/{testutils → dist/testutils}/db/fixture.js +0 -0
- /package/{testutils → dist/testutils}/db/temp_db.d.ts +0 -0
- /package/{testutils → dist/testutils}/db/temp_db.js +0 -0
- /package/{testutils → dist/testutils}/db/value.d.ts +0 -0
- /package/{testutils → dist/testutils}/db/value.js +0 -0
- /package/{testutils → dist/testutils}/db_mock.d.ts +0 -0
- /package/{testutils → dist/testutils}/db_mock.js +0 -0
- /package/{testutils → dist/testutils}/db_time_zone.d.ts +0 -0
- /package/{testutils → dist/testutils}/db_time_zone.js +0 -0
- /package/{testutils → dist/testutils}/ent-graphql-tests/index.d.ts +0 -0
- /package/{testutils → dist/testutils}/ent-graphql-tests/index.js +0 -0
- /package/{testutils → dist/testutils}/fake_comms.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_comms.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/const.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/const.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/events_query.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/events_query.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_contact.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_contact.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_event.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_event.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_tag.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_tag.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_user.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/fake_user.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/index.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/index.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/internal.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/internal.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/tag_query.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/tag_query.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/test_helpers.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/test_helpers.js +0 -0
- /package/{testutils → dist/testutils}/fake_data/user_query.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_data/user_query.js +0 -0
- /package/{testutils → dist/testutils}/fake_log.d.ts +0 -0
- /package/{testutils → dist/testutils}/fake_log.js +0 -0
- /package/{testutils → dist/testutils}/mock_date.d.ts +0 -0
- /package/{testutils → dist/testutils}/mock_date.js +0 -0
- /package/{testutils → dist/testutils}/mock_log.d.ts +0 -0
- /package/{testutils → dist/testutils}/mock_log.js +0 -0
- /package/{testutils → dist/testutils}/parse_sql.d.ts +0 -0
- /package/{testutils → dist/testutils}/parse_sql.js +0 -0
- /package/{testutils → dist/testutils}/test_edge_global_schema.d.ts +0 -0
- /package/{testutils → dist/testutils}/test_edge_global_schema.js +0 -0
- /package/{testutils → dist/testutils}/write.d.ts +0 -0
- /package/{testutils → dist/testutils}/write.js +0 -0
- /package/{tsc → dist/tsc}/ast.d.ts +0 -0
- /package/{tsc → dist/tsc}/ast.js +0 -0
- /package/{tsc → dist/tsc}/compilerOptions.d.ts +0 -0
- /package/{tsc → dist/tsc}/compilerOptions.js +0 -0
- /package/{tsc → dist/tsc}/move_generated.d.ts +0 -0
- /package/{tsc → dist/tsc}/move_generated.js +0 -0
- /package/{tsc → dist/tsc}/transform.d.ts +0 -0
- /package/{tsc → dist/tsc}/transform.js +0 -0
- /package/{tsc → dist/tsc}/transform_action.d.ts +0 -0
- /package/{tsc → dist/tsc}/transform_action.js +0 -0
- /package/{tsc → dist/tsc}/transform_ent.d.ts +0 -0
- /package/{tsc → dist/tsc}/transform_ent.js +0 -0
- /package/{tsc → dist/tsc}/transform_schema.d.ts +0 -0
- /package/{tsc → dist/tsc}/transform_schema.js +0 -0
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ID,
|
|
3
|
+
Data,
|
|
4
|
+
Ent,
|
|
5
|
+
Viewer,
|
|
6
|
+
LoadEntOptions,
|
|
7
|
+
PrivacyError,
|
|
8
|
+
PrivacyPolicy,
|
|
9
|
+
CreateRowOptions,
|
|
10
|
+
} from "../core/base";
|
|
11
|
+
import {
|
|
12
|
+
loadEdgeDatas,
|
|
13
|
+
applyPrivacyPolicyForRow,
|
|
14
|
+
parameterizedQueryOptions,
|
|
15
|
+
loadEdgeData,
|
|
16
|
+
} from "../core/ent";
|
|
17
|
+
import {
|
|
18
|
+
getFields,
|
|
19
|
+
SchemaInputType,
|
|
20
|
+
Field,
|
|
21
|
+
getTransformedUpdateOp,
|
|
22
|
+
SQLStatementOperation,
|
|
23
|
+
TransformedUpdateOperation,
|
|
24
|
+
FieldInfoMap,
|
|
25
|
+
getFieldsWithEditPrivacy,
|
|
26
|
+
getFieldsForCreateAction,
|
|
27
|
+
} from "../schema/schema";
|
|
28
|
+
import {
|
|
29
|
+
Changeset,
|
|
30
|
+
ChangesetOptions,
|
|
31
|
+
Executor,
|
|
32
|
+
Validator,
|
|
33
|
+
} from "../action/action";
|
|
34
|
+
import {
|
|
35
|
+
AssocEdgeInputOptions,
|
|
36
|
+
DataOperation,
|
|
37
|
+
EdgeOperation,
|
|
38
|
+
EditNodeOperation,
|
|
39
|
+
DeleteNodeOperation,
|
|
40
|
+
EditNodeOptions,
|
|
41
|
+
AssocEdgeOptions,
|
|
42
|
+
ConditionalOperation,
|
|
43
|
+
ConditionalNodeOperation,
|
|
44
|
+
} from "./operations";
|
|
45
|
+
import { WriteOperation, Builder, Action } from "../action";
|
|
46
|
+
import { applyPrivacyPolicy, applyPrivacyPolicyX } from "../core/privacy";
|
|
47
|
+
import { ListBasedExecutor, ComplexExecutor } from "./executor";
|
|
48
|
+
import { log } from "../core/logger";
|
|
49
|
+
import { Trigger } from "./action";
|
|
50
|
+
import memoize from "memoizee";
|
|
51
|
+
import * as clause from "../core/clause";
|
|
52
|
+
import { isPromise } from "util/types";
|
|
53
|
+
import { RawQueryOperation } from "./operations";
|
|
54
|
+
|
|
55
|
+
type MaybeNull<T extends Ent> = T | null;
|
|
56
|
+
type TMaybleNullableEnt<T extends Ent> = T | MaybeNull<T>;
|
|
57
|
+
|
|
58
|
+
export interface OrchestratorOptions<
|
|
59
|
+
TEnt extends Ent<TViewer>,
|
|
60
|
+
TInput extends Data,
|
|
61
|
+
TViewer extends Viewer,
|
|
62
|
+
TExistingEnt extends TMaybleNullableEnt<TEnt> = MaybeNull<TEnt>,
|
|
63
|
+
> {
|
|
64
|
+
viewer: TViewer;
|
|
65
|
+
operation: WriteOperation;
|
|
66
|
+
tableName: string;
|
|
67
|
+
// should we make it nullable for delete?
|
|
68
|
+
loaderOptions: LoadEntOptions<TEnt, TViewer>;
|
|
69
|
+
// key, usually 'id' that's being updated
|
|
70
|
+
key: string;
|
|
71
|
+
|
|
72
|
+
builder: Builder<TEnt, TViewer, TExistingEnt>;
|
|
73
|
+
action?: Action<TEnt, Builder<TEnt, TViewer>, TViewer, TInput>;
|
|
74
|
+
schema: SchemaInputType;
|
|
75
|
+
editedFields(): Map<string, any> | Promise<Map<string, any>>;
|
|
76
|
+
// this is called with fields with defaultValueOnCreate|Edit
|
|
77
|
+
updateInput?: (data: TInput) => void;
|
|
78
|
+
|
|
79
|
+
// mapping of column to expressions to use
|
|
80
|
+
// if set and a column exists, we use the expression here instead of the given expression in the sql query
|
|
81
|
+
// for now, only works in an `UPDATE` query i.e. with operation === WriteOperation.Insert
|
|
82
|
+
// if passed with a different operation type, it throws an Error
|
|
83
|
+
// any value provided in editedFields for this value is ignored and we assume the right thing is done with said expression
|
|
84
|
+
|
|
85
|
+
// TODO ability to get expression value, parse it and update it
|
|
86
|
+
// e.g. if somehow there's a promotion if you play your 1000th game (which costs 5 tokens),
|
|
87
|
+
// we increase your balance by 1000000 after the cost of the ticket
|
|
88
|
+
// or we completely use your balance or something
|
|
89
|
+
expressions?: Map<string, clause.Clause>;
|
|
90
|
+
fieldInfo: FieldInfoMap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface edgeInputDataOpts {
|
|
94
|
+
edgeType: string;
|
|
95
|
+
id: Builder<Ent> | ID; // when an OutboundEdge, this is the id2, when an inbound edge, this is the id1
|
|
96
|
+
nodeType?: string; // expected to be set for WriteOperation.Insert and undefined for WriteOperation.Delete
|
|
97
|
+
options?: AssocEdgeInputOptions;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// hmm is it worth having multiple types here or just having one?
|
|
101
|
+
// we have one type here instead
|
|
102
|
+
export interface EdgeInputData extends edgeInputDataOpts {
|
|
103
|
+
isBuilder(id: Builder<Ent> | ID): id is Builder<Ent>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export enum edgeDirection {
|
|
107
|
+
inboundEdge,
|
|
108
|
+
outboundEdge,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface internalEdgeInputData extends edgeInputDataOpts {
|
|
112
|
+
direction: edgeDirection;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class edgeInputData implements EdgeInputData {
|
|
116
|
+
direction: edgeDirection;
|
|
117
|
+
edgeType: string;
|
|
118
|
+
id: Builder<Ent> | ID;
|
|
119
|
+
nodeType?: string;
|
|
120
|
+
options?: AssocEdgeInputOptions;
|
|
121
|
+
|
|
122
|
+
constructor(opts: internalEdgeInputData) {
|
|
123
|
+
Object.assign(this, opts);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
isBuilder(id: Builder<Ent> | ID): id is Builder<Ent> {
|
|
127
|
+
return (id as Builder<Ent>).placeholderID !== undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type IDMap = Map<ID, edgeInputData>;
|
|
132
|
+
type OperationMap = Map<WriteOperation, IDMap>;
|
|
133
|
+
// this is a map of
|
|
134
|
+
// edgeType : {
|
|
135
|
+
// WriteOperation: {
|
|
136
|
+
// id: {
|
|
137
|
+
// id input
|
|
138
|
+
// }
|
|
139
|
+
// }
|
|
140
|
+
// }
|
|
141
|
+
type EdgeMap = Map<string, OperationMap>;
|
|
142
|
+
|
|
143
|
+
function getViewer(viewer: Viewer) {
|
|
144
|
+
if (!viewer.viewerID) {
|
|
145
|
+
return "Logged out Viewer";
|
|
146
|
+
} else {
|
|
147
|
+
return `Viewer with ID ${viewer.viewerID}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class EntCannotCreateEntError extends Error implements PrivacyError {
|
|
152
|
+
privacyPolicy: PrivacyPolicy;
|
|
153
|
+
constructor(privacyPolicy: PrivacyPolicy, action: Action<Ent, Builder<Ent>>) {
|
|
154
|
+
let msg = `${getViewer(action.viewer)} does not have permission to create ${
|
|
155
|
+
action.builder.ent.name
|
|
156
|
+
}`;
|
|
157
|
+
super(msg);
|
|
158
|
+
this.privacyPolicy = privacyPolicy;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class EntCannotEditEntError extends Error implements PrivacyError {
|
|
163
|
+
privacyPolicy: PrivacyPolicy;
|
|
164
|
+
constructor(
|
|
165
|
+
privacyPolicy: PrivacyPolicy,
|
|
166
|
+
action: Action<Ent, Builder<Ent>>,
|
|
167
|
+
ent: Ent,
|
|
168
|
+
) {
|
|
169
|
+
let msg = `${getViewer(action.viewer)} does not have permission to edit ${
|
|
170
|
+
ent.constructor.name
|
|
171
|
+
}`;
|
|
172
|
+
super(msg);
|
|
173
|
+
this.privacyPolicy = privacyPolicy;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class EntCannotEditEntFieldError extends Error implements PrivacyError {
|
|
178
|
+
privacyPolicy: PrivacyPolicy;
|
|
179
|
+
constructor(
|
|
180
|
+
privacyPolicy: PrivacyPolicy,
|
|
181
|
+
viewer: Viewer,
|
|
182
|
+
field: string,
|
|
183
|
+
ent: Ent,
|
|
184
|
+
) {
|
|
185
|
+
let msg = `${getViewer(
|
|
186
|
+
viewer,
|
|
187
|
+
)} does not have permission to edit field ${field} in ${
|
|
188
|
+
ent.constructor.name
|
|
189
|
+
}`;
|
|
190
|
+
super(msg);
|
|
191
|
+
this.privacyPolicy = privacyPolicy;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class EntCannotDeleteEntError extends Error implements PrivacyError {
|
|
196
|
+
privacyPolicy: PrivacyPolicy;
|
|
197
|
+
constructor(
|
|
198
|
+
privacyPolicy: PrivacyPolicy,
|
|
199
|
+
action: Action<Ent, Builder<Ent>>,
|
|
200
|
+
ent: Ent,
|
|
201
|
+
) {
|
|
202
|
+
let msg = `${getViewer(action.viewer)} does not have permission to delete ${
|
|
203
|
+
ent.constructor.name
|
|
204
|
+
}`;
|
|
205
|
+
super(msg);
|
|
206
|
+
this.privacyPolicy = privacyPolicy;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface fieldsInfo {
|
|
211
|
+
editedData: Data;
|
|
212
|
+
editedFields: Map<string, any>;
|
|
213
|
+
schemaFields: Map<string, Field>;
|
|
214
|
+
userDefinedKeys: Set<string>;
|
|
215
|
+
editPrivacyFields: Map<string, PrivacyPolicy>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class Orchestrator<
|
|
219
|
+
TEnt extends Ent<TViewer>,
|
|
220
|
+
TInput extends Data,
|
|
221
|
+
TViewer extends Viewer,
|
|
222
|
+
TExistingEnt extends TMaybleNullableEnt<TEnt> = MaybeNull<TEnt>,
|
|
223
|
+
> {
|
|
224
|
+
private edgeSet: Set<string> = new Set<string>();
|
|
225
|
+
private edges: EdgeMap = new Map();
|
|
226
|
+
private conditionalEdges: EdgeMap = new Map();
|
|
227
|
+
private validatedFields: Data | null;
|
|
228
|
+
private logValues: Data | null;
|
|
229
|
+
private changesets: Changeset[] = [];
|
|
230
|
+
private dependencies: Map<ID, Builder<TEnt>> = new Map();
|
|
231
|
+
private fieldsToResolve: string[] = [];
|
|
232
|
+
private mainOp: DataOperation<TEnt> | null;
|
|
233
|
+
viewer: Viewer;
|
|
234
|
+
private defaultFieldsByFieldName: Data = {};
|
|
235
|
+
private defaultFieldsByTSName: Data = {};
|
|
236
|
+
// we can transform from one update to another so we wanna differentiate
|
|
237
|
+
// btw the beginning op and the transformed one we end up using
|
|
238
|
+
private actualOperation: WriteOperation;
|
|
239
|
+
// same with existingEnt. can transform so we wanna know what we started with and now where we are.
|
|
240
|
+
private existingEnt: TExistingEnt;
|
|
241
|
+
private disableTransformations: boolean;
|
|
242
|
+
private onConflict: CreateRowOptions["onConflict"] | undefined;
|
|
243
|
+
private memoizedGetFields: () => Promise<fieldsInfo>;
|
|
244
|
+
|
|
245
|
+
constructor(
|
|
246
|
+
private options: OrchestratorOptions<TEnt, TInput, TViewer, TExistingEnt>,
|
|
247
|
+
) {
|
|
248
|
+
this.viewer = options.viewer;
|
|
249
|
+
this.actualOperation = this.options.operation;
|
|
250
|
+
this.existingEnt = this.options.builder.existingEnt;
|
|
251
|
+
this.memoizedGetFields = memoize(this.getFieldsInfo.bind(this));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// don't type this because we don't care
|
|
255
|
+
__getOptions(): OrchestratorOptions<any, any, any, any> {
|
|
256
|
+
return this.options;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private addEdge(
|
|
260
|
+
edge: edgeInputData,
|
|
261
|
+
op: WriteOperation,
|
|
262
|
+
conditional?: boolean,
|
|
263
|
+
) {
|
|
264
|
+
this.edgeSet.add(edge.edgeType);
|
|
265
|
+
|
|
266
|
+
let m1: OperationMap = this.edges.get(edge.edgeType) || new Map();
|
|
267
|
+
let m2: IDMap = m1.get(op) || new Map();
|
|
268
|
+
let id: ID;
|
|
269
|
+
if (edge.isBuilder(edge.id)) {
|
|
270
|
+
id = edge.id.placeholderID;
|
|
271
|
+
} else {
|
|
272
|
+
id = edge.id;
|
|
273
|
+
}
|
|
274
|
+
// let id = edge.id.toString(); // TODO confirm that toString for builder is placeholderID. if not, add it or change this...
|
|
275
|
+
// set or overwrite the new edge data for said id
|
|
276
|
+
m2.set(id, edge);
|
|
277
|
+
m1.set(op, m2);
|
|
278
|
+
if (conditional && this.onConflict) {
|
|
279
|
+
this.conditionalEdges.set(edge.edgeType, m1);
|
|
280
|
+
} else {
|
|
281
|
+
this.edges.set(edge.edgeType, m1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setDisableTransformations(val: boolean) {
|
|
286
|
+
this.disableTransformations = val;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
setOnConflictOptions(onConflict: CreateRowOptions["onConflict"]) {
|
|
290
|
+
if (onConflict?.onConflictConstraint && !onConflict.updateCols) {
|
|
291
|
+
throw new Error(`cannot set onConflictConstraint without updateCols`);
|
|
292
|
+
}
|
|
293
|
+
this.onConflict = onConflict;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
addInboundEdge<T2 extends Ent>(
|
|
297
|
+
id1: ID | Builder<T2, any>,
|
|
298
|
+
edgeType: string,
|
|
299
|
+
nodeType: string,
|
|
300
|
+
options?: AssocEdgeInputOptions,
|
|
301
|
+
) {
|
|
302
|
+
this.addEdge(
|
|
303
|
+
new edgeInputData({
|
|
304
|
+
id: id1,
|
|
305
|
+
edgeType,
|
|
306
|
+
nodeType,
|
|
307
|
+
options,
|
|
308
|
+
direction: edgeDirection.inboundEdge,
|
|
309
|
+
}),
|
|
310
|
+
WriteOperation.Insert,
|
|
311
|
+
options?.conditional,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
addOutboundEdge<T2 extends Ent>(
|
|
316
|
+
id2: ID | Builder<T2, any>,
|
|
317
|
+
edgeType: string,
|
|
318
|
+
nodeType: string,
|
|
319
|
+
options?: AssocEdgeInputOptions,
|
|
320
|
+
) {
|
|
321
|
+
this.addEdge(
|
|
322
|
+
new edgeInputData({
|
|
323
|
+
id: id2,
|
|
324
|
+
edgeType,
|
|
325
|
+
nodeType,
|
|
326
|
+
options,
|
|
327
|
+
direction: edgeDirection.outboundEdge,
|
|
328
|
+
}),
|
|
329
|
+
WriteOperation.Insert,
|
|
330
|
+
options?.conditional,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
removeInboundEdge(id1: ID, edgeType: string, options?: AssocEdgeOptions) {
|
|
335
|
+
this.addEdge(
|
|
336
|
+
new edgeInputData({
|
|
337
|
+
id: id1,
|
|
338
|
+
edgeType,
|
|
339
|
+
direction: edgeDirection.inboundEdge,
|
|
340
|
+
options,
|
|
341
|
+
}),
|
|
342
|
+
WriteOperation.Delete,
|
|
343
|
+
options?.conditional,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
removeOutboundEdge(id2: ID, edgeType: string, options?: AssocEdgeOptions) {
|
|
348
|
+
this.addEdge(
|
|
349
|
+
new edgeInputData({
|
|
350
|
+
id: id2,
|
|
351
|
+
edgeType,
|
|
352
|
+
direction: edgeDirection.outboundEdge,
|
|
353
|
+
options,
|
|
354
|
+
}),
|
|
355
|
+
WriteOperation.Delete,
|
|
356
|
+
options?.conditional,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// this doesn't take a direction as that's an implementation detail
|
|
361
|
+
// it doesn't make any sense to use the same edgeType for inbound and outbound edges
|
|
362
|
+
// so no need for that
|
|
363
|
+
getInputEdges(edgeType: string, op: WriteOperation): EdgeInputData[] {
|
|
364
|
+
let m: IDMap = this.edges.get(edgeType)?.get(op) || new Map();
|
|
365
|
+
// want a list and not an IterableIterator
|
|
366
|
+
let ret: edgeInputData[] = [];
|
|
367
|
+
m.forEach((v) => ret.push(v));
|
|
368
|
+
|
|
369
|
+
return ret;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// this privides a way to clear data if needed
|
|
373
|
+
// we don't have a great API for this yet
|
|
374
|
+
clearInputEdges(edgeType: string, op: WriteOperation, id?: ID) {
|
|
375
|
+
let m: IDMap = this.edges.get(edgeType)?.get(op) || new Map();
|
|
376
|
+
if (id) {
|
|
377
|
+
m.delete(id);
|
|
378
|
+
} else {
|
|
379
|
+
m.clear();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private buildMainOp(conditionalBuilder?: Builder<any>): DataOperation {
|
|
384
|
+
// this assumes we have validated fields
|
|
385
|
+
switch (this.actualOperation) {
|
|
386
|
+
case WriteOperation.Delete:
|
|
387
|
+
return new DeleteNodeOperation(
|
|
388
|
+
this.existingEnt!.id,
|
|
389
|
+
this.options.builder,
|
|
390
|
+
{
|
|
391
|
+
tableName: this.options.tableName,
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
default:
|
|
395
|
+
if (this.actualOperation === WriteOperation.Edit && !this.existingEnt) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`existing ent required with operation ${this.actualOperation}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (
|
|
401
|
+
this.options.expressions &&
|
|
402
|
+
this.actualOperation !== WriteOperation.Edit
|
|
403
|
+
) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`expressions are only supported in edit operations for now`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const opts: EditNodeOptions<TEnt> = {
|
|
409
|
+
fields: this.validatedFields!,
|
|
410
|
+
tableName: this.options.tableName,
|
|
411
|
+
fieldsToResolve: this.fieldsToResolve,
|
|
412
|
+
key: this.options.key,
|
|
413
|
+
loadEntOptions: this.options.loaderOptions,
|
|
414
|
+
whereClause: clause.Eq(this.options.key, this.existingEnt?.id),
|
|
415
|
+
expressions: this.options.expressions,
|
|
416
|
+
onConflict: this.onConflict,
|
|
417
|
+
builder: this.options.builder,
|
|
418
|
+
};
|
|
419
|
+
if (this.logValues) {
|
|
420
|
+
opts.fieldsToLog = this.logValues;
|
|
421
|
+
}
|
|
422
|
+
this.mainOp = new EditNodeOperation(opts, this.existingEnt);
|
|
423
|
+
if (conditionalBuilder) {
|
|
424
|
+
this.mainOp = new ConditionalNodeOperation(
|
|
425
|
+
this.mainOp,
|
|
426
|
+
conditionalBuilder,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return this.mainOp;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// edgeType e.g. EdgeType.OrganizationToArchivedMembers
|
|
434
|
+
// add | remove
|
|
435
|
+
// operation e.g. WriteOperation.Insert or WriteOperation.Delete
|
|
436
|
+
// and then what's the format to return and how do we deal with placeholders?
|
|
437
|
+
// { id: ID | Builder<Ent>}
|
|
438
|
+
// or we push the resolving to the end and return the raw data?
|
|
439
|
+
// seems like the best approach...
|
|
440
|
+
// so if you pass a builder, you get it back
|
|
441
|
+
// and can pass it to the other e.g. removeEdge
|
|
442
|
+
//
|
|
443
|
+
private getEdgeOperation(
|
|
444
|
+
edgeType: string,
|
|
445
|
+
op: WriteOperation,
|
|
446
|
+
edge: internalEdgeInputData,
|
|
447
|
+
): EdgeOperation {
|
|
448
|
+
if (op === WriteOperation.Insert) {
|
|
449
|
+
if (!edge.nodeType) {
|
|
450
|
+
throw new Error(`no nodeType for edge when adding outboundEdge`);
|
|
451
|
+
}
|
|
452
|
+
if (edge.direction === edgeDirection.outboundEdge) {
|
|
453
|
+
return EdgeOperation.outboundEdge(
|
|
454
|
+
this.options.builder,
|
|
455
|
+
edgeType,
|
|
456
|
+
edge.id,
|
|
457
|
+
edge.nodeType,
|
|
458
|
+
edge.options,
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
return EdgeOperation.inboundEdge(
|
|
462
|
+
this.options.builder,
|
|
463
|
+
edgeType,
|
|
464
|
+
edge.id,
|
|
465
|
+
edge.nodeType,
|
|
466
|
+
edge.options,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
} else if (op === WriteOperation.Delete) {
|
|
470
|
+
if (this.isBuilder(edge.id)) {
|
|
471
|
+
throw new Error("removeEdge APIs don't take a builder as an argument");
|
|
472
|
+
}
|
|
473
|
+
let id2 = edge.id as ID;
|
|
474
|
+
|
|
475
|
+
if (edge.direction === edgeDirection.outboundEdge) {
|
|
476
|
+
return EdgeOperation.removeOutboundEdge(
|
|
477
|
+
this.options.builder,
|
|
478
|
+
edgeType,
|
|
479
|
+
id2,
|
|
480
|
+
edge.options,
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
return EdgeOperation.removeInboundEdge(
|
|
484
|
+
this.options.builder,
|
|
485
|
+
edgeType,
|
|
486
|
+
id2,
|
|
487
|
+
edge.options,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
throw new Error(
|
|
492
|
+
"could not find an edge operation from the given parameters",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private async buildEdgeOps(
|
|
497
|
+
ops: DataOperation[],
|
|
498
|
+
conditionalBuilder: Builder<any>,
|
|
499
|
+
conditionalOverride: boolean,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
const edgeDatas = await loadEdgeDatas(...Array.from(this.edgeSet.values()));
|
|
502
|
+
const edges: [EdgeMap, boolean][] = [
|
|
503
|
+
[this.edges, false],
|
|
504
|
+
[this.conditionalEdges, true],
|
|
505
|
+
];
|
|
506
|
+
// conditional should only apply if onconflict...
|
|
507
|
+
// if no upsert and just create, nothing to do here
|
|
508
|
+
for (const edgeInfo of edges) {
|
|
509
|
+
const [edges, conditionalEdge] = edgeInfo;
|
|
510
|
+
const conditional = conditionalOverride || conditionalEdge;
|
|
511
|
+
for (const [edgeType, m] of edges) {
|
|
512
|
+
for (const [op, m2] of m) {
|
|
513
|
+
for (const [_, edge] of m2) {
|
|
514
|
+
let edgeOp = this.getEdgeOperation(edgeType, op, edge);
|
|
515
|
+
if (conditional) {
|
|
516
|
+
ops.push(new ConditionalOperation(edgeOp, conditionalBuilder));
|
|
517
|
+
} else {
|
|
518
|
+
ops.push(edgeOp);
|
|
519
|
+
}
|
|
520
|
+
const edgeData = edgeDatas.get(edgeType);
|
|
521
|
+
if (!edgeData) {
|
|
522
|
+
throw new Error(`could not load edge data for '${edgeType}'`);
|
|
523
|
+
}
|
|
524
|
+
// similar logic in EntChangeset.changesetFromEdgeOp
|
|
525
|
+
// doesn't support conditional edges
|
|
526
|
+
|
|
527
|
+
if (edgeData.symmetricEdge) {
|
|
528
|
+
let symmetric: DataOperation = edgeOp.symmetricEdge();
|
|
529
|
+
if (conditional) {
|
|
530
|
+
symmetric = new ConditionalOperation(
|
|
531
|
+
symmetric,
|
|
532
|
+
conditionalBuilder,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
ops.push(symmetric);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (edgeData.inverseEdgeType) {
|
|
539
|
+
let inverse: DataOperation = edgeOp.inverseEdge(edgeData);
|
|
540
|
+
if (conditional) {
|
|
541
|
+
inverse = new ConditionalOperation(inverse, conditionalBuilder);
|
|
542
|
+
}
|
|
543
|
+
ops.push(inverse);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private throwError(): PrivacyError {
|
|
552
|
+
const action = this.options.action;
|
|
553
|
+
let privacyPolicy = action?.getPrivacyPolicy();
|
|
554
|
+
if (!privacyPolicy || !action) {
|
|
555
|
+
throw new Error(`shouldn't get here if no privacyPolicy for action`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (this.actualOperation === WriteOperation.Insert) {
|
|
559
|
+
return new EntCannotCreateEntError(privacyPolicy, action);
|
|
560
|
+
} else if (this.actualOperation === WriteOperation.Edit) {
|
|
561
|
+
return new EntCannotEditEntError(
|
|
562
|
+
privacyPolicy,
|
|
563
|
+
action,
|
|
564
|
+
this.existingEnt!,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return new EntCannotDeleteEntError(
|
|
568
|
+
privacyPolicy,
|
|
569
|
+
action,
|
|
570
|
+
this.existingEnt!,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private async getRowForPrivacyPolicyImpl(
|
|
575
|
+
schemaFields: Map<string, Field>,
|
|
576
|
+
editedData: Data,
|
|
577
|
+
): Promise<Data> {
|
|
578
|
+
// need to format fields if possible because ent constructors expect data that's
|
|
579
|
+
// in the format that's coming from the db
|
|
580
|
+
// required for object fields...
|
|
581
|
+
|
|
582
|
+
const formatted = { ...editedData };
|
|
583
|
+
for (const [fieldName, field] of schemaFields) {
|
|
584
|
+
if (!field.format) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let dbKey = this.getStorageKey(fieldName);
|
|
589
|
+
let val = formatted[dbKey];
|
|
590
|
+
if (!val) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (field.valid) {
|
|
595
|
+
let valid = field.valid(val);
|
|
596
|
+
if (isPromise(valid)) {
|
|
597
|
+
valid = await valid;
|
|
598
|
+
}
|
|
599
|
+
// if not valid, don't format and don't pass to ent?
|
|
600
|
+
// or just early throw here
|
|
601
|
+
if (!valid) {
|
|
602
|
+
continue;
|
|
603
|
+
// throw new Error(`invalid field ${fieldName} with value ${val}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// nested so it's not JSON stringified or anything like that
|
|
608
|
+
val = field.format(formatted[dbKey], true);
|
|
609
|
+
if (isPromise(val)) {
|
|
610
|
+
val = await val;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
formatted[dbKey] = val;
|
|
614
|
+
}
|
|
615
|
+
return formatted;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private async getEntForPrivacyPolicyImpl(
|
|
619
|
+
schemaFields: Map<string, Field>,
|
|
620
|
+
editedData: Data,
|
|
621
|
+
viewerToUse: TViewer,
|
|
622
|
+
rowToUse?: Data,
|
|
623
|
+
): Promise<TEnt> {
|
|
624
|
+
if (this.actualOperation !== WriteOperation.Insert) {
|
|
625
|
+
return this.existingEnt!;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!rowToUse) {
|
|
629
|
+
rowToUse = await this.getRowForPrivacyPolicyImpl(
|
|
630
|
+
schemaFields,
|
|
631
|
+
editedData,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// we create an unsafe ent to be used for privacy policies
|
|
636
|
+
return new this.options.builder.ent(viewerToUse, rowToUse);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private getSQLStatementOperation(): SQLStatementOperation {
|
|
640
|
+
switch (this.actualOperation) {
|
|
641
|
+
case WriteOperation.Edit:
|
|
642
|
+
return SQLStatementOperation.Update;
|
|
643
|
+
case WriteOperation.Insert:
|
|
644
|
+
return SQLStatementOperation.Insert;
|
|
645
|
+
case WriteOperation.Delete:
|
|
646
|
+
return SQLStatementOperation.Delete;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private getWriteOpForSQLStamentOp(op: SQLStatementOperation): WriteOperation {
|
|
651
|
+
switch (op) {
|
|
652
|
+
case SQLStatementOperation.Update:
|
|
653
|
+
return WriteOperation.Edit;
|
|
654
|
+
case SQLStatementOperation.Insert:
|
|
655
|
+
return WriteOperation.Insert;
|
|
656
|
+
case SQLStatementOperation.Update:
|
|
657
|
+
return WriteOperation.Delete;
|
|
658
|
+
default:
|
|
659
|
+
throw new Error("invalid path");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// if you're doing custom privacy within an action and want to
|
|
664
|
+
// get either the unsafe ent or the existing ent that's being edited
|
|
665
|
+
async getPossibleUnsafeEntForPrivacy(): Promise<TEnt> {
|
|
666
|
+
if (this.actualOperation !== WriteOperation.Insert) {
|
|
667
|
+
return this.existingEnt!;
|
|
668
|
+
}
|
|
669
|
+
const { schemaFields, editedData } = await this.memoizedGetFields();
|
|
670
|
+
return this.getEntForPrivacyPolicyImpl(
|
|
671
|
+
schemaFields,
|
|
672
|
+
editedData,
|
|
673
|
+
this.options.viewer,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// this gets the fields that were explicitly set plus any default or transformed values
|
|
678
|
+
// mainly exists to get default fields e.g. default id to be used in triggers
|
|
679
|
+
// NOTE: this API may change in the future
|
|
680
|
+
// doesn't work to get ids for autoincrement keys
|
|
681
|
+
async getEditedData() {
|
|
682
|
+
const { editedData } = await this.memoizedGetFields();
|
|
683
|
+
return editedData;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @returns validated and formatted fields that would be written to the db
|
|
688
|
+
* throws an error if called before valid() or validX() has been called
|
|
689
|
+
*/
|
|
690
|
+
getValidatedFields() {
|
|
691
|
+
if (this.validatedFields === null) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`trying to call getValidatedFields before validating fields`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return this.validatedFields;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Note: this is memoized. call memoizedGetFields instead
|
|
700
|
+
private async getFieldsInfo(): Promise<fieldsInfo> {
|
|
701
|
+
const action = this.options.action;
|
|
702
|
+
const builder = this.options.builder;
|
|
703
|
+
|
|
704
|
+
// future optimization: can get schemaFields to memoize based on different values
|
|
705
|
+
const schemaFields = getFields(this.options.schema);
|
|
706
|
+
|
|
707
|
+
// also future optimization, no need to go through the list of fields multiple times
|
|
708
|
+
let editPrivacyFields = new Map<string, PrivacyPolicy>();
|
|
709
|
+
switch (this.actualOperation) {
|
|
710
|
+
case WriteOperation.Edit:
|
|
711
|
+
editPrivacyFields = getFieldsWithEditPrivacy(
|
|
712
|
+
this.options.schema,
|
|
713
|
+
this.options.fieldInfo,
|
|
714
|
+
);
|
|
715
|
+
break;
|
|
716
|
+
|
|
717
|
+
case WriteOperation.Insert:
|
|
718
|
+
editPrivacyFields = getFieldsForCreateAction(
|
|
719
|
+
this.options.schema,
|
|
720
|
+
this.options.fieldInfo,
|
|
721
|
+
);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const editedFields = await this.options.editedFields();
|
|
726
|
+
|
|
727
|
+
let { data: editedData, userDefinedKeys } =
|
|
728
|
+
await this.getFieldsWithDefaultValues(
|
|
729
|
+
builder,
|
|
730
|
+
schemaFields,
|
|
731
|
+
editedFields,
|
|
732
|
+
action,
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
editedData,
|
|
737
|
+
editedFields,
|
|
738
|
+
schemaFields,
|
|
739
|
+
userDefinedKeys,
|
|
740
|
+
editPrivacyFields,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private async validate(): Promise<Error[]> {
|
|
745
|
+
// existing ent required for edit or delete operations
|
|
746
|
+
switch (this.actualOperation) {
|
|
747
|
+
case WriteOperation.Delete:
|
|
748
|
+
case WriteOperation.Edit:
|
|
749
|
+
if (!this.existingEnt) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`existing ent required with operation ${this.actualOperation}`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const { schemaFields, editedData, userDefinedKeys, editPrivacyFields } =
|
|
757
|
+
await this.memoizedGetFields();
|
|
758
|
+
const action = this.options.action;
|
|
759
|
+
const builder = this.options.builder;
|
|
760
|
+
|
|
761
|
+
// this runs in following phases:
|
|
762
|
+
// * set default fields and pass to builder so the value can be checked by triggers/observers/validators
|
|
763
|
+
// * privacy policy (use unsafe ent if we have it)
|
|
764
|
+
// * triggers
|
|
765
|
+
// * validators
|
|
766
|
+
let privacyPolicy = action?.getPrivacyPolicy();
|
|
767
|
+
|
|
768
|
+
const errors: Error[] = [];
|
|
769
|
+
|
|
770
|
+
if (privacyPolicy) {
|
|
771
|
+
const ent = await this.getEntForPrivacyPolicyImpl(
|
|
772
|
+
schemaFields,
|
|
773
|
+
editedData,
|
|
774
|
+
this.options.viewer,
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
await applyPrivacyPolicyX(this.options.viewer, privacyPolicy, ent, () =>
|
|
779
|
+
this.throwError(),
|
|
780
|
+
);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
errors.push(err);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// we have edit privacy fields, so we need to apply privacy policy on those
|
|
787
|
+
const promises: Promise<void>[] = [];
|
|
788
|
+
if (editPrivacyFields.size) {
|
|
789
|
+
// get row based on edited data
|
|
790
|
+
const row = await this.getRowForPrivacyPolicyImpl(
|
|
791
|
+
schemaFields,
|
|
792
|
+
editedData,
|
|
793
|
+
);
|
|
794
|
+
// get viewer for ent load based on formatted row
|
|
795
|
+
const viewer = await this.viewerForEntLoad(row);
|
|
796
|
+
|
|
797
|
+
const ent = await this.getEntForPrivacyPolicyImpl(
|
|
798
|
+
schemaFields,
|
|
799
|
+
editedData,
|
|
800
|
+
viewer,
|
|
801
|
+
row,
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
for (const [k, policy] of editPrivacyFields) {
|
|
805
|
+
if (editedData[k] === undefined || !userDefinedKeys.has(k)) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
promises.push(
|
|
809
|
+
(async () => {
|
|
810
|
+
const r = await applyPrivacyPolicy(viewer, policy, ent);
|
|
811
|
+
if (!r) {
|
|
812
|
+
errors.push(
|
|
813
|
+
new EntCannotEditEntFieldError(policy, viewer, k, ent!),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
})(),
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
await Promise.all(promises);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// privacy or field errors should return first so it's less confusing
|
|
823
|
+
if (errors.length) {
|
|
824
|
+
return errors;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// have to run triggers which update fields first before field and other validators
|
|
828
|
+
// so running this first to build things up
|
|
829
|
+
if (action?.getTriggers) {
|
|
830
|
+
await this.triggers(action!, builder, action.getTriggers());
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
let validators: Validator<TEnt, Builder<TEnt, TViewer>, TViewer, TInput>[] =
|
|
834
|
+
[];
|
|
835
|
+
if (action?.getValidators) {
|
|
836
|
+
validators = action.getValidators();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// not ideal we're calling this twice. fix...
|
|
840
|
+
// needed for now. may need to rewrite some of this?
|
|
841
|
+
const editedFields2 = await this.options.editedFields();
|
|
842
|
+
const [errs2, errs3] = await Promise.all([
|
|
843
|
+
this.formatAndValidateFields(schemaFields, editedFields2),
|
|
844
|
+
this.validators(validators, action!, builder),
|
|
845
|
+
]);
|
|
846
|
+
errors.push(...errs2);
|
|
847
|
+
errors.push(...errs3);
|
|
848
|
+
return errors;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private async triggers(
|
|
852
|
+
action: Action<TEnt, Builder<TEnt, TViewer>, TViewer, TInput>,
|
|
853
|
+
builder: Builder<TEnt, TViewer>,
|
|
854
|
+
triggers: Array<
|
|
855
|
+
| Trigger<TEnt, Builder<TEnt, TViewer>>
|
|
856
|
+
| Array<Trigger<TEnt, Builder<TEnt, TViewer>>>
|
|
857
|
+
>,
|
|
858
|
+
): Promise<void> {
|
|
859
|
+
let groups: Trigger<TEnt, Builder<TEnt, TViewer>>[][] = [];
|
|
860
|
+
let lastArray = 0;
|
|
861
|
+
let prevWasArray = false;
|
|
862
|
+
for (let i = 0; i < triggers.length; i++) {
|
|
863
|
+
let t = triggers[i];
|
|
864
|
+
if (Array.isArray(t)) {
|
|
865
|
+
if (!prevWasArray) {
|
|
866
|
+
// @ts-ignore
|
|
867
|
+
groups.push(triggers.slice(lastArray, i));
|
|
868
|
+
}
|
|
869
|
+
groups.push(t);
|
|
870
|
+
|
|
871
|
+
prevWasArray = true;
|
|
872
|
+
lastArray++;
|
|
873
|
+
} else {
|
|
874
|
+
if (i === triggers.length - 1) {
|
|
875
|
+
// @ts-ignore
|
|
876
|
+
groups.push(triggers.slice(lastArray, i + 1));
|
|
877
|
+
}
|
|
878
|
+
prevWasArray = false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
for (const triggers of groups) {
|
|
883
|
+
await Promise.all(
|
|
884
|
+
triggers.map(async (trigger) => {
|
|
885
|
+
let ret = await trigger.changeset(builder, action.getInput());
|
|
886
|
+
if (Array.isArray(ret)) {
|
|
887
|
+
ret = await Promise.all(ret);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (Array.isArray(ret)) {
|
|
891
|
+
for (const v of ret) {
|
|
892
|
+
if (typeof v === "object") {
|
|
893
|
+
this.changesets.push(v);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} else if (ret) {
|
|
897
|
+
this.changesets.push(ret);
|
|
898
|
+
}
|
|
899
|
+
}),
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private async validators(
|
|
905
|
+
validators: Validator<TEnt, Builder<TEnt, TViewer>, TViewer, TInput>[],
|
|
906
|
+
action: Action<TEnt, Builder<TEnt, TViewer>, TViewer, TInput>,
|
|
907
|
+
builder: Builder<TEnt, TViewer>,
|
|
908
|
+
): Promise<Error[]> {
|
|
909
|
+
const errors: Error[] = [];
|
|
910
|
+
await Promise.all(
|
|
911
|
+
validators.map(async (v) => {
|
|
912
|
+
try {
|
|
913
|
+
const r = await v.validate(builder, action.getInput());
|
|
914
|
+
if (r instanceof Error) {
|
|
915
|
+
errors.push(r);
|
|
916
|
+
}
|
|
917
|
+
} catch (err) {
|
|
918
|
+
errors.push(err as Error);
|
|
919
|
+
}
|
|
920
|
+
}),
|
|
921
|
+
);
|
|
922
|
+
return errors;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private isBuilder(val: Builder<TEnt> | any): val is Builder<TEnt> {
|
|
926
|
+
return (val as Builder<TEnt>).placeholderID !== undefined;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private getInputKey(k: string) {
|
|
930
|
+
return this.options.fieldInfo[k].inputKey;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private getStorageKey(k: string) {
|
|
934
|
+
return this.options.fieldInfo[k].dbCol;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private async getFieldsWithDefaultValues(
|
|
938
|
+
builder: Builder<TEnt, TViewer>,
|
|
939
|
+
schemaFields: Map<string, Field>,
|
|
940
|
+
editedFields: Map<string, any>,
|
|
941
|
+
action?: Action<TEnt, Builder<TEnt, TViewer>, TViewer, TInput> | undefined,
|
|
942
|
+
): Promise<Data> {
|
|
943
|
+
let data: Data = {};
|
|
944
|
+
let defaultData: Data = {};
|
|
945
|
+
|
|
946
|
+
let input: Data = action?.getInput() || {};
|
|
947
|
+
|
|
948
|
+
let updateInput = false;
|
|
949
|
+
|
|
950
|
+
// transformations
|
|
951
|
+
// if action transformations. always do it
|
|
952
|
+
// if disable transformations set, don't do schema transform and just do the right thing
|
|
953
|
+
// else apply schema tranformation if it exists
|
|
954
|
+
let transformed: TransformedUpdateOperation<TEnt, TViewer> | null = null;
|
|
955
|
+
|
|
956
|
+
const sqlOp = this.getSQLStatementOperation();
|
|
957
|
+
// why is transform write technically different from upsert?
|
|
958
|
+
// it's create -> update just at the db level...
|
|
959
|
+
if (action?.transformWrite) {
|
|
960
|
+
transformed = await action.transformWrite({
|
|
961
|
+
builder,
|
|
962
|
+
input,
|
|
963
|
+
op: sqlOp,
|
|
964
|
+
data: editedFields,
|
|
965
|
+
});
|
|
966
|
+
} else if (!this.disableTransformations) {
|
|
967
|
+
transformed = getTransformedUpdateOp<TEnt, TViewer>(this.options.schema, {
|
|
968
|
+
builder,
|
|
969
|
+
input,
|
|
970
|
+
op: sqlOp,
|
|
971
|
+
data: editedFields,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (transformed) {
|
|
975
|
+
if (sqlOp === SQLStatementOperation.Insert && sqlOp !== transformed.op) {
|
|
976
|
+
if (!transformed.existingEnt) {
|
|
977
|
+
throw new Error(
|
|
978
|
+
`cannot transform an insert operation without providing an existing ent`,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (transformed.data) {
|
|
983
|
+
updateInput = true;
|
|
984
|
+
for (const k in transformed.data) {
|
|
985
|
+
let field = schemaFields.get(k);
|
|
986
|
+
if (!field) {
|
|
987
|
+
throw new Error(`tried to transform field with unknown field ${k}`);
|
|
988
|
+
}
|
|
989
|
+
let val = transformed.data[k];
|
|
990
|
+
if (field.format) {
|
|
991
|
+
val = field.format(transformed.data[k]);
|
|
992
|
+
}
|
|
993
|
+
data[this.getStorageKey(k)] = val;
|
|
994
|
+
this.defaultFieldsByTSName[this.getInputKey(k)] = val;
|
|
995
|
+
// hmm do we need this?
|
|
996
|
+
// TODO how to do this for local tests?
|
|
997
|
+
// this.defaultFieldsByFieldName[k] = val;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (transformed.changeset) {
|
|
1001
|
+
const changeset = await transformed.changeset();
|
|
1002
|
+
this.changesets.push(changeset);
|
|
1003
|
+
}
|
|
1004
|
+
this.actualOperation = this.getWriteOpForSQLStamentOp(transformed.op);
|
|
1005
|
+
if (transformed.existingEnt) {
|
|
1006
|
+
// @ts-ignore
|
|
1007
|
+
this.existingEnt = transformed.existingEnt;
|
|
1008
|
+
// modify existing ent in builder. it's readonly in generated ents but doesn't apply here
|
|
1009
|
+
builder.existingEnt = transformed.existingEnt;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// transforming before doing default fields so that we don't create a new id
|
|
1013
|
+
// and anything that depends on the type of operations knows what it is
|
|
1014
|
+
|
|
1015
|
+
const userDefinedKeys = new Set<string>();
|
|
1016
|
+
for (const [fieldName, field] of schemaFields) {
|
|
1017
|
+
let value = editedFields.get(fieldName);
|
|
1018
|
+
let defaultValue: any = undefined;
|
|
1019
|
+
let dbKey = this.getStorageKey(fieldName);
|
|
1020
|
+
|
|
1021
|
+
let updateOnlyIfOther = field.onlyUpdateIfOtherFieldsBeingSet_BETA;
|
|
1022
|
+
|
|
1023
|
+
if (value !== undefined) {
|
|
1024
|
+
userDefinedKeys.add(dbKey);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (value === undefined) {
|
|
1028
|
+
if (this.actualOperation === WriteOperation.Insert) {
|
|
1029
|
+
if (field.defaultToViewerOnCreate && field.defaultValueOnCreate) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`cannot set both defaultToViewerOnCreate and defaultValueOnCreate`,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
if (field.defaultToViewerOnCreate) {
|
|
1035
|
+
defaultValue = builder.viewer.viewerID;
|
|
1036
|
+
}
|
|
1037
|
+
if (field.defaultValueOnCreate) {
|
|
1038
|
+
defaultValue = field.defaultValueOnCreate(builder, input);
|
|
1039
|
+
if (defaultValue === undefined) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`defaultValueOnCreate() returned undefined for field ${fieldName}`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
if (isPromise(defaultValue)) {
|
|
1045
|
+
defaultValue = await defaultValue;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (
|
|
1051
|
+
field.defaultValueOnEdit &&
|
|
1052
|
+
this.actualOperation === WriteOperation.Edit
|
|
1053
|
+
) {
|
|
1054
|
+
defaultValue = field.defaultValueOnEdit(builder, input);
|
|
1055
|
+
if (isPromise(defaultValue)) {
|
|
1056
|
+
defaultValue = await defaultValue;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (value !== undefined) {
|
|
1062
|
+
data[dbKey] = value;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (defaultValue !== undefined) {
|
|
1066
|
+
updateInput = true;
|
|
1067
|
+
|
|
1068
|
+
if (updateOnlyIfOther) {
|
|
1069
|
+
defaultData[dbKey] = defaultValue;
|
|
1070
|
+
} else {
|
|
1071
|
+
data[dbKey] = defaultValue;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
this.defaultFieldsByFieldName[fieldName] = defaultValue;
|
|
1075
|
+
this.defaultFieldsByTSName[this.getInputKey(fieldName)] = defaultValue;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// if there's data changing, add data
|
|
1080
|
+
if (this.hasData(data)) {
|
|
1081
|
+
data = {
|
|
1082
|
+
...data,
|
|
1083
|
+
...defaultData,
|
|
1084
|
+
};
|
|
1085
|
+
if (updateInput && this.options.updateInput) {
|
|
1086
|
+
// this basically fixes #605. just needs to be exposed correctly
|
|
1087
|
+
this.options.updateInput(this.defaultFieldsByTSName as TInput);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return { data, userDefinedKeys };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private hasData(data: Data) {
|
|
1095
|
+
for (const _k in data) {
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private async transformFieldValue(
|
|
1102
|
+
fieldName: string,
|
|
1103
|
+
field: Field,
|
|
1104
|
+
dbKey: string,
|
|
1105
|
+
value: any,
|
|
1106
|
+
): Promise<Error | any> {
|
|
1107
|
+
// now format and validate...
|
|
1108
|
+
if (value === null) {
|
|
1109
|
+
if (!field.nullable) {
|
|
1110
|
+
return new Error(
|
|
1111
|
+
`field ${fieldName} set to null for non-nullable field`,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
} else if (value === undefined) {
|
|
1115
|
+
if (
|
|
1116
|
+
!field.nullable &&
|
|
1117
|
+
// required field can be skipped if server default set
|
|
1118
|
+
// not checking defaultValueOnCreate() or defaultValueOnEdit() as that's set above
|
|
1119
|
+
// not setting server default as we're depending on the database handling that.
|
|
1120
|
+
// server default allowed
|
|
1121
|
+
field.serverDefault === undefined &&
|
|
1122
|
+
this.actualOperation === WriteOperation.Insert
|
|
1123
|
+
) {
|
|
1124
|
+
return new Error(`required field ${fieldName} not set`);
|
|
1125
|
+
}
|
|
1126
|
+
} else if (this.isBuilder(value)) {
|
|
1127
|
+
if (field.valid) {
|
|
1128
|
+
let valid = field.valid(value);
|
|
1129
|
+
if (isPromise(valid)) {
|
|
1130
|
+
valid = await valid;
|
|
1131
|
+
}
|
|
1132
|
+
if (!valid) {
|
|
1133
|
+
return new Error(`invalid field ${fieldName} with value ${value}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// keep track of dependencies to resolve
|
|
1137
|
+
this.dependencies.set(value.placeholderID, value);
|
|
1138
|
+
// keep track of fields to resolve
|
|
1139
|
+
this.fieldsToResolve.push(dbKey);
|
|
1140
|
+
} else {
|
|
1141
|
+
if (field.valid) {
|
|
1142
|
+
let valid = field.valid(value);
|
|
1143
|
+
if (isPromise(valid)) {
|
|
1144
|
+
valid = await valid;
|
|
1145
|
+
}
|
|
1146
|
+
if (!valid) {
|
|
1147
|
+
return new Error(`invalid field ${fieldName} with value ${value}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (field.format) {
|
|
1152
|
+
value = await field.format(value);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return value;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private async formatAndValidateFields(
|
|
1159
|
+
schemaFields: Map<string, Field>,
|
|
1160
|
+
editedFields: Map<string, any>,
|
|
1161
|
+
): Promise<Error[]> {
|
|
1162
|
+
const errors: Error[] = [];
|
|
1163
|
+
const op = this.actualOperation;
|
|
1164
|
+
if (op === WriteOperation.Delete) {
|
|
1165
|
+
return [];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// build up data to be saved...
|
|
1169
|
+
let data = {};
|
|
1170
|
+
let logValues = {};
|
|
1171
|
+
|
|
1172
|
+
let needsFullDataChecks: string[] = [];
|
|
1173
|
+
for (const [fieldName, field] of schemaFields) {
|
|
1174
|
+
let value = editedFields.get(fieldName);
|
|
1175
|
+
|
|
1176
|
+
if (field.validateWithFullData) {
|
|
1177
|
+
needsFullDataChecks.push(fieldName);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (value === undefined && op === WriteOperation.Insert) {
|
|
1181
|
+
// null allowed
|
|
1182
|
+
value = this.defaultFieldsByFieldName[fieldName];
|
|
1183
|
+
}
|
|
1184
|
+
let dbKey = this.getStorageKey(fieldName);
|
|
1185
|
+
|
|
1186
|
+
let ret = await this.transformFieldValue(fieldName, field, dbKey, value);
|
|
1187
|
+
if (ret instanceof Error) {
|
|
1188
|
+
errors.push(ret);
|
|
1189
|
+
} else {
|
|
1190
|
+
value = ret;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (value !== undefined) {
|
|
1194
|
+
data[dbKey] = value;
|
|
1195
|
+
logValues[dbKey] = field.logValue(value);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
for (const fieldName of needsFullDataChecks) {
|
|
1200
|
+
const field = schemaFields.get(fieldName)!;
|
|
1201
|
+
let value = editedFields.get(fieldName);
|
|
1202
|
+
|
|
1203
|
+
// @ts-ignore...
|
|
1204
|
+
// type hackery because it's hard
|
|
1205
|
+
const v = await field.validateWithFullData(value, this.options.builder);
|
|
1206
|
+
if (!v) {
|
|
1207
|
+
if (value === undefined) {
|
|
1208
|
+
errors.push(
|
|
1209
|
+
new Error(
|
|
1210
|
+
`field ${fieldName} set to undefined when it can't be nullable`,
|
|
1211
|
+
),
|
|
1212
|
+
);
|
|
1213
|
+
} else {
|
|
1214
|
+
errors.push(
|
|
1215
|
+
new Error(
|
|
1216
|
+
`field ${fieldName} set to null when it can't be nullable`,
|
|
1217
|
+
),
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// we ignored default values while editing.
|
|
1224
|
+
// if we're editing and there's data, add default values
|
|
1225
|
+
if (op === WriteOperation.Edit && this.hasData(data)) {
|
|
1226
|
+
for (const fieldName in this.defaultFieldsByFieldName) {
|
|
1227
|
+
const defaultValue = this.defaultFieldsByFieldName[fieldName];
|
|
1228
|
+
let field = schemaFields.get(fieldName)!;
|
|
1229
|
+
|
|
1230
|
+
let dbKey = this.getStorageKey(fieldName);
|
|
1231
|
+
|
|
1232
|
+
// no value, let's just default
|
|
1233
|
+
if (data[dbKey] === undefined) {
|
|
1234
|
+
const ret = await this.transformFieldValue(
|
|
1235
|
+
fieldName,
|
|
1236
|
+
field,
|
|
1237
|
+
dbKey,
|
|
1238
|
+
defaultValue,
|
|
1239
|
+
);
|
|
1240
|
+
if (ret instanceof Error) {
|
|
1241
|
+
errors.push(ret);
|
|
1242
|
+
} else {
|
|
1243
|
+
data[dbKey] = ret;
|
|
1244
|
+
logValues[dbKey] = field.logValue(ret);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
this.validatedFields = data;
|
|
1251
|
+
this.logValues = logValues;
|
|
1252
|
+
return errors;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
async valid(): Promise<boolean> {
|
|
1256
|
+
const errors = await this.validate();
|
|
1257
|
+
if (errors.length) {
|
|
1258
|
+
errors.map((err) => log("error", err));
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async validX(): Promise<void> {
|
|
1265
|
+
const errors = await this.validate();
|
|
1266
|
+
if (errors.length) {
|
|
1267
|
+
// just throw the first one...
|
|
1268
|
+
// TODO we should ideally throw all of them
|
|
1269
|
+
throw errors[0];
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* @experimental API that's not guaranteed to remain in the future which returns
|
|
1275
|
+
* a list of errors encountered
|
|
1276
|
+
* 0 errors indicates valid
|
|
1277
|
+
* NOTE that this currently doesn't catch errors returned by validators().
|
|
1278
|
+
* If those throws, this still throws and doesn't return them
|
|
1279
|
+
*/
|
|
1280
|
+
async validWithErrors(): Promise<Error[]> {
|
|
1281
|
+
return this.validate();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
private async buildPlusChangeset(
|
|
1285
|
+
conditionalBuilder: Builder<any>,
|
|
1286
|
+
conditionalOverride: boolean,
|
|
1287
|
+
): Promise<EntChangeset<TEnt>> {
|
|
1288
|
+
// validate everything first
|
|
1289
|
+
await this.validX();
|
|
1290
|
+
|
|
1291
|
+
let ops: DataOperation[] = [
|
|
1292
|
+
this.buildMainOp(conditionalOverride ? conditionalBuilder : undefined),
|
|
1293
|
+
];
|
|
1294
|
+
|
|
1295
|
+
await this.buildEdgeOps(ops, conditionalBuilder, conditionalOverride);
|
|
1296
|
+
|
|
1297
|
+
// TODO throw if we try and create a new changeset after previously creating one
|
|
1298
|
+
|
|
1299
|
+
// TODO test actualOperation value
|
|
1300
|
+
// observers is fine since they're run after and we have the actualOperation value...
|
|
1301
|
+
|
|
1302
|
+
return new EntChangeset(
|
|
1303
|
+
this.options.viewer,
|
|
1304
|
+
this.options.builder,
|
|
1305
|
+
this.options.builder.placeholderID,
|
|
1306
|
+
conditionalOverride,
|
|
1307
|
+
ops,
|
|
1308
|
+
this.dependencies,
|
|
1309
|
+
this.changesets,
|
|
1310
|
+
this.options,
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async build(): Promise<EntChangeset<TEnt>> {
|
|
1315
|
+
return this.buildPlusChangeset(this.options.builder, false);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async buildWithOptions_BETA(
|
|
1319
|
+
options: ChangesetOptions,
|
|
1320
|
+
): Promise<EntChangeset<TEnt>> {
|
|
1321
|
+
// set as dependency so that we do the right order of operations
|
|
1322
|
+
this.dependencies.set(
|
|
1323
|
+
options.conditionalBuilder.placeholderID,
|
|
1324
|
+
options.conditionalBuilder,
|
|
1325
|
+
);
|
|
1326
|
+
return this.buildPlusChangeset(options.conditionalBuilder, true);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
private async viewerForEntLoad(data: Data) {
|
|
1330
|
+
const action = this.options.action;
|
|
1331
|
+
if (!action || !action.viewerForEntLoad) {
|
|
1332
|
+
return this.options.viewer;
|
|
1333
|
+
}
|
|
1334
|
+
return action.viewerForEntLoad(data, action.builder.viewer.context);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async returnedRow(): Promise<Data | null> {
|
|
1338
|
+
if (this.mainOp && this.mainOp.returnedRow) {
|
|
1339
|
+
return this.mainOp.returnedRow();
|
|
1340
|
+
}
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async editedEnt(): Promise<TEnt | null> {
|
|
1345
|
+
const row = await this.returnedRow();
|
|
1346
|
+
if (!row) {
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
const viewer = await this.viewerForEntLoad(row);
|
|
1350
|
+
return applyPrivacyPolicyForRow(viewer, this.options.loaderOptions, row);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async editedEntX(): Promise<TEnt> {
|
|
1354
|
+
const row = await this.returnedRow();
|
|
1355
|
+
if (!row) {
|
|
1356
|
+
throw new Error(`ent was not created`);
|
|
1357
|
+
}
|
|
1358
|
+
const viewer = await this.viewerForEntLoad(row);
|
|
1359
|
+
const ent = await applyPrivacyPolicyForRow(
|
|
1360
|
+
viewer,
|
|
1361
|
+
this.options.loaderOptions,
|
|
1362
|
+
row,
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
if (!ent) {
|
|
1366
|
+
if (this.actualOperation == WriteOperation.Insert) {
|
|
1367
|
+
throw new Error(`was able to create ent but not load it`);
|
|
1368
|
+
} else {
|
|
1369
|
+
throw new Error(`was able to edit ent but not load it`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return ent;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function randomNum(): string {
|
|
1377
|
+
return Math.random().toString(10).substring(2);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// each changeset is required to have a unique placeholderID
|
|
1381
|
+
// used in executor. if we end up creating multiple changesets from a builder, we need
|
|
1382
|
+
// different placeholders
|
|
1383
|
+
// in practice, only applies to Entchangeset::changesetFrom()
|
|
1384
|
+
export class EntChangeset<T extends Ent> implements Changeset {
|
|
1385
|
+
private _executor: Executor | null;
|
|
1386
|
+
constructor(
|
|
1387
|
+
public viewer: Viewer,
|
|
1388
|
+
private builder: Builder<T>,
|
|
1389
|
+
public readonly placeholderID: ID,
|
|
1390
|
+
private conditionalOverride: boolean,
|
|
1391
|
+
public operations: DataOperation[],
|
|
1392
|
+
public dependencies?: Map<ID, Builder<Ent>>,
|
|
1393
|
+
public changesets?: Changeset[],
|
|
1394
|
+
private options?: OrchestratorOptions<T, Data, Viewer>,
|
|
1395
|
+
) {}
|
|
1396
|
+
|
|
1397
|
+
static changesetFrom(builder: Builder<any, any, any>, ops: DataOperation[]) {
|
|
1398
|
+
return new EntChangeset(
|
|
1399
|
+
builder.viewer,
|
|
1400
|
+
builder,
|
|
1401
|
+
// need unique placeholderID different from the builder. see comment above EntChangeset
|
|
1402
|
+
`$ent.idPlaceholderID$ ${randomNum()}-${builder.ent.name}`,
|
|
1403
|
+
false,
|
|
1404
|
+
ops,
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
static changesetFromQueries(
|
|
1409
|
+
builder: Builder<any, any, any>,
|
|
1410
|
+
queries: Array<string | parameterizedQueryOptions>,
|
|
1411
|
+
) {
|
|
1412
|
+
return EntChangeset.changesetFrom(builder, [
|
|
1413
|
+
new RawQueryOperation(builder, queries),
|
|
1414
|
+
]);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
private static async changesetFromEdgeOp(
|
|
1418
|
+
builder: Builder<any, any, any>,
|
|
1419
|
+
op: EdgeOperation,
|
|
1420
|
+
edgeType: string,
|
|
1421
|
+
) {
|
|
1422
|
+
const edgeData = await loadEdgeData(edgeType);
|
|
1423
|
+
const ops: DataOperation[] = [op];
|
|
1424
|
+
if (!edgeData) {
|
|
1425
|
+
throw new Error(`could not load edge data for '${edgeType}'`);
|
|
1426
|
+
}
|
|
1427
|
+
// similar logic in Orchestrator.buildEdgeOps
|
|
1428
|
+
// doesn't support conditional edges
|
|
1429
|
+
if (edgeData.symmetricEdge) {
|
|
1430
|
+
ops.push(op.symmetricEdge());
|
|
1431
|
+
}
|
|
1432
|
+
if (edgeData.inverseEdgeType) {
|
|
1433
|
+
ops.push(op.inverseEdge(edgeData));
|
|
1434
|
+
}
|
|
1435
|
+
return EntChangeset.changesetFrom(builder, ops);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
static async changesetFromOutboundEdge(
|
|
1439
|
+
builder: Builder<any, any, any>,
|
|
1440
|
+
edgeType: string,
|
|
1441
|
+
id2: Builder<any> | ID,
|
|
1442
|
+
nodeType: string,
|
|
1443
|
+
options?: AssocEdgeInputOptions,
|
|
1444
|
+
) {
|
|
1445
|
+
return EntChangeset.changesetFromEdgeOp(
|
|
1446
|
+
builder,
|
|
1447
|
+
EdgeOperation.outboundEdge(builder, edgeType, id2, nodeType, options),
|
|
1448
|
+
edgeType,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
static async changesetFromInboundEdge(
|
|
1453
|
+
builder: Builder<any, any, any>,
|
|
1454
|
+
edgeType: string,
|
|
1455
|
+
id1: Builder<any> | ID,
|
|
1456
|
+
nodeType: string,
|
|
1457
|
+
options?: AssocEdgeInputOptions,
|
|
1458
|
+
) {
|
|
1459
|
+
return EntChangeset.changesetFromEdgeOp(
|
|
1460
|
+
builder,
|
|
1461
|
+
EdgeOperation.inboundEdge(builder, edgeType, id1, nodeType, options),
|
|
1462
|
+
edgeType,
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
static changesetRemoveFromOutboundEdge(
|
|
1467
|
+
builder: Builder<any, any, any>,
|
|
1468
|
+
edgeType: string,
|
|
1469
|
+
id2: ID,
|
|
1470
|
+
options?: AssocEdgeInputOptions,
|
|
1471
|
+
) {
|
|
1472
|
+
return EntChangeset.changesetFromEdgeOp(
|
|
1473
|
+
builder,
|
|
1474
|
+
EdgeOperation.removeOutboundEdge(builder, edgeType, id2, options),
|
|
1475
|
+
edgeType,
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
static changesetRemoveFromInboundEdge(
|
|
1480
|
+
builder: Builder<any, any, any>,
|
|
1481
|
+
edgeType: string,
|
|
1482
|
+
id1: ID,
|
|
1483
|
+
options?: AssocEdgeInputOptions,
|
|
1484
|
+
) {
|
|
1485
|
+
return EntChangeset.changesetFromEdgeOp(
|
|
1486
|
+
builder,
|
|
1487
|
+
EdgeOperation.removeInboundEdge(builder, edgeType, id1, options),
|
|
1488
|
+
edgeType,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
executor(): Executor {
|
|
1493
|
+
if (this._executor) {
|
|
1494
|
+
return this._executor;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (!this.changesets?.length) {
|
|
1498
|
+
// if we have dependencies but no changesets, we just need a simple
|
|
1499
|
+
// executor and depend on something else in the stack to handle this correctly
|
|
1500
|
+
// ComplexExecutor which could be a parent of this should make sure the dependency
|
|
1501
|
+
// is resolved beforehand
|
|
1502
|
+
return (this._executor = new ListBasedExecutor(
|
|
1503
|
+
this.viewer,
|
|
1504
|
+
this.placeholderID,
|
|
1505
|
+
this.operations,
|
|
1506
|
+
this.options,
|
|
1507
|
+
{
|
|
1508
|
+
conditionalOverride: this.conditionalOverride,
|
|
1509
|
+
builder: this.builder,
|
|
1510
|
+
},
|
|
1511
|
+
));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
return (this._executor = new ComplexExecutor(
|
|
1515
|
+
this.viewer,
|
|
1516
|
+
this.placeholderID,
|
|
1517
|
+
this.operations,
|
|
1518
|
+
this.dependencies || new Map(),
|
|
1519
|
+
this.changesets || [],
|
|
1520
|
+
this.options,
|
|
1521
|
+
{
|
|
1522
|
+
conditionalOverride: this.conditionalOverride,
|
|
1523
|
+
builder: this.builder,
|
|
1524
|
+
},
|
|
1525
|
+
));
|
|
1526
|
+
}
|
|
1527
|
+
}
|