@antelopejs/interface-data-api 0.4.0 → 0.6.0
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/README.md +26 -22
- package/dist/antelope.test.js +1 -5
- package/dist/antelope.test.js.map +1 -1
- package/dist/components.d.ts +67 -39
- package/dist/components.js +106 -685
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +57 -68
- package/dist/index.js +114 -100
- package/dist/index.js.map +1 -1
- package/dist/internal/computed.d.ts +6 -0
- package/dist/internal/computed.js +51 -0
- package/dist/internal/computed.js.map +1 -0
- package/dist/internal/controller-types.d.ts +34 -0
- package/dist/internal/controller-types.js +4 -0
- package/dist/internal/controller-types.js.map +1 -0
- package/dist/internal/default-filter.d.ts +4 -0
- package/dist/internal/default-filter.js +29 -0
- package/dist/internal/default-filter.js.map +1 -0
- package/dist/internal/deferred-joined-stream.d.ts +11 -0
- package/dist/internal/deferred-joined-stream.js +125 -0
- package/dist/internal/deferred-joined-stream.js.map +1 -0
- package/dist/internal/extractors.d.ts +25 -0
- package/dist/internal/extractors.js +31 -0
- package/dist/internal/extractors.js.map +1 -0
- package/dist/internal/field-types.d.ts +199 -0
- package/dist/internal/field-types.js +4 -0
- package/dist/internal/field-types.js.map +1 -0
- package/dist/internal/filters.d.ts +6 -0
- package/dist/internal/filters.js +22 -0
- package/dist/internal/filters.js.map +1 -0
- package/dist/internal/foreign.d.ts +6 -0
- package/dist/internal/foreign.js +116 -0
- package/dist/internal/foreign.js.map +1 -0
- package/dist/internal/guards.d.ts +39 -0
- package/dist/internal/guards.js +116 -0
- package/dist/internal/guards.js.map +1 -0
- package/dist/internal/joined.d.ts +28 -0
- package/dist/internal/joined.js +127 -0
- package/dist/internal/joined.js.map +1 -0
- package/dist/internal/list.d.ts +16 -0
- package/dist/internal/list.js +182 -0
- package/dist/internal/list.js.map +1 -0
- package/dist/internal/meta-records.d.ts +19 -0
- package/dist/internal/meta-records.js +78 -0
- package/dist/internal/meta-records.js.map +1 -0
- package/dist/internal/properties.d.ts +4 -0
- package/dist/internal/properties.js +45 -0
- package/dist/internal/properties.js.map +1 -0
- package/dist/internal/prototypes.d.ts +14 -0
- package/dist/internal/prototypes.js +38 -0
- package/dist/internal/prototypes.js.map +1 -0
- package/dist/internal/read-properties.d.ts +3 -0
- package/dist/internal/read-properties.js +29 -0
- package/dist/internal/read-properties.js.map +1 -0
- package/dist/internal/table-reference.d.ts +11 -0
- package/dist/internal/table-reference.js +46 -0
- package/dist/internal/table-reference.js.map +1 -0
- package/dist/internal/tables.d.ts +28 -0
- package/dist/internal/tables.js +48 -0
- package/dist/internal/tables.js.map +1 -0
- package/dist/internal/validation.d.ts +12 -0
- package/dist/internal/validation.js +100 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/metadata.d.ts +27 -228
- package/dist/metadata.js +84 -213
- package/dist/metadata.js.map +1 -1
- package/dist/tests/components/access_control.test.js +28 -20
- package/dist/tests/components/access_control.test.js.map +1 -1
- package/dist/tests/components/computed.test.js +36 -27
- package/dist/tests/components/computed.test.js.map +1 -1
- package/dist/tests/components/cross_instance.test.js +8 -6
- package/dist/tests/components/cross_instance.test.js.map +1 -1
- package/dist/tests/components/deferred_joined.test.js +57 -54
- package/dist/tests/components/deferred_joined.test.js.map +1 -1
- package/dist/tests/components/extract_generic.test.d.ts +1 -0
- package/dist/tests/components/extract_generic.test.js +64 -0
- package/dist/tests/components/extract_generic.test.js.map +1 -0
- package/dist/tests/components/filter_comparisons.test.d.ts +1 -0
- package/dist/tests/components/filter_comparisons.test.js +34 -0
- package/dist/tests/components/filter_comparisons.test.js.map +1 -0
- package/dist/tests/components/filter_list_route.test.d.ts +1 -0
- package/dist/tests/components/filter_list_route.test.js +182 -0
- package/dist/tests/components/filter_list_route.test.js.map +1 -0
- package/dist/tests/components/filter_options.test.js +11 -10
- package/dist/tests/components/filter_options.test.js.map +1 -1
- package/dist/tests/components/foreign_computed.test.js +23 -14
- package/dist/tests/components/foreign_computed.test.js.map +1 -1
- package/dist/tests/components/foreign_joined.test.js +31 -27
- package/dist/tests/components/foreign_joined.test.js.map +1 -1
- package/dist/tests/components/joined.test.js +59 -45
- package/dist/tests/components/joined.test.js.map +1 -1
- package/dist/tests/components/listable.test.js +78 -58
- package/dist/tests/components/listable.test.js.map +1 -1
- package/dist/tests/components/localized_sort.test.js +14 -11
- package/dist/tests/components/localized_sort.test.js.map +1 -1
- package/dist/tests/components/mandatory.test.js +25 -25
- package/dist/tests/components/mandatory.test.js.map +1 -1
- package/dist/tests/components/modifier_key.test.js +27 -18
- package/dist/tests/components/modifier_key.test.js.map +1 -1
- package/dist/tests/components/parse_body.test.d.ts +1 -0
- package/dist/tests/components/parse_body.test.js +76 -0
- package/dist/tests/components/parse_body.test.js.map +1 -0
- package/dist/tests/components/parsed_value.test.js +23 -21
- package/dist/tests/components/parsed_value.test.js.map +1 -1
- package/dist/tests/components/partial_edit.test.js +38 -32
- package/dist/tests/components/partial_edit.test.js.map +1 -1
- package/dist/tests/components/per_action_access.test.js +20 -12
- package/dist/tests/components/per_action_access.test.js.map +1 -1
- package/dist/tests/components/prototype_named_filter.test.d.ts +1 -0
- package/dist/tests/components/prototype_named_filter.test.js +87 -0
- package/dist/tests/components/prototype_named_filter.test.js.map +1 -0
- package/dist/tests/components/validator.test.js +45 -37
- package/dist/tests/components/validator.test.js.map +1 -1
- package/dist/tests/components/written_row.test.d.ts +1 -0
- package/dist/tests/components/written_row.test.js +99 -0
- package/dist/tests/components/written_row.test.js.map +1 -0
- package/dist/tests/helpers/other-instance.d.ts +11 -1
- package/dist/tests/helpers/other-instance.js +35 -9
- package/dist/tests/helpers/other-instance.js.map +1 -1
- package/dist/tests/index/field_decorator_target.test.d.ts +1 -0
- package/dist/tests/index/field_decorator_target.test.js +20 -0
- package/dist/tests/index/field_decorator_target.test.js.map +1 -0
- package/dist/tests/index/get_guard.test.js +6 -6
- package/dist/tests/index/get_guard.test.js.map +1 -1
- package/dist/tests/index/inheritance.test.js +14 -9
- package/dist/tests/index/inheritance.test.js.map +1 -1
- package/dist/tests/index/metadata_names.test.d.ts +1 -0
- package/dist/tests/index/metadata_names.test.js +91 -0
- package/dist/tests/index/metadata_names.test.js.map +1 -0
- package/dist/tests/index/routes.test.js +49 -44
- package/dist/tests/index/routes.test.js.map +1 -1
- package/dist/tests/integration.test.d.ts +1 -1
- package/dist/tests/integration.test.js +66 -729
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/internal/controller-fields.test.d.ts +1 -0
- package/dist/tests/internal/controller-fields.test.js +25 -0
- package/dist/tests/internal/controller-fields.test.js.map +1 -0
- package/dist/tests/internal/http.d.ts +35 -0
- package/dist/tests/internal/http.js +56 -0
- package/dist/tests/internal/http.js.map +1 -0
- package/dist/tests/internal/shop-api.d.ts +1 -0
- package/dist/tests/internal/shop-api.js +331 -0
- package/dist/tests/internal/shop-api.js.map +1 -0
- package/dist/tests/internal/shop-schema.d.ts +162 -0
- package/dist/tests/internal/shop-schema.js +387 -0
- package/dist/tests/internal/shop-schema.js.map +1 -0
- package/dist/tests/utils.d.ts +8 -10
- package/dist/tests/utils.js +1 -3
- package/dist/tests/utils.js.map +1 -1
- package/dist/tests/utils.test.js +10 -11
- package/dist/tests/utils.test.js.map +1 -1
- package/docs/1.introduction.md +38 -35
- package/docs/10.joined.md +51 -40
- package/docs/11.computed.md +91 -47
- package/docs/2.data-controllers.md +50 -42
- package/docs/3.routes.md +156 -134
- package/docs/4.access-rights.md +48 -44
- package/docs/5.validators.md +65 -47
- package/docs/6.listable.md +53 -49
- package/docs/7.foreign-keys.md +55 -48
- package/docs/8.filters.md +65 -54
- package/docs/9.modifiers.md +29 -25
- package/package.json +34 -13
- package/skills/data-api-interface/SKILL.md +13 -3
- package/skills/data-api-interface/cases.json +99 -0
package/dist/components.js
CHANGED
|
@@ -1,29 +1,65 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.Validation = exports.Query = exports.Parameters = void 0;
|
|
4
37
|
const interface_api_util_1 = require("@antelopejs/interface-api-util");
|
|
5
38
|
const common_1 = require("@antelopejs/interface-database-decorators/common");
|
|
6
39
|
const decorators_1 = require("@antelopejs/interface-core/decorators");
|
|
7
40
|
const interface_api_1 = require("@antelopejs/interface-api");
|
|
8
|
-
|
|
41
|
+
require("@antelopejs/interface-database");
|
|
9
42
|
const common_2 = require("@antelopejs/interface-database-decorators/modifiers/common");
|
|
10
43
|
const metadata_1 = require("./metadata");
|
|
44
|
+
const computed = __importStar(require("./internal/computed"));
|
|
45
|
+
const filters_1 = require("./internal/filters");
|
|
46
|
+
const foreign = __importStar(require("./internal/foreign"));
|
|
47
|
+
const extractors = __importStar(require("./internal/extractors"));
|
|
48
|
+
const guards_1 = require("./internal/guards");
|
|
49
|
+
const joined = __importStar(require("./internal/joined"));
|
|
50
|
+
const list = __importStar(require("./internal/list"));
|
|
51
|
+
const properties = __importStar(require("./internal/properties"));
|
|
52
|
+
const prototypes_1 = require("./internal/prototypes");
|
|
53
|
+
const read_properties_1 = require("./internal/read-properties");
|
|
54
|
+
const tables = __importStar(require("./internal/tables"));
|
|
55
|
+
const validation = __importStar(require("./internal/validation"));
|
|
11
56
|
var Parameters;
|
|
12
57
|
(function (Parameters) {
|
|
58
|
+
/** The options the route's `WithOptions` set, none when it set none. */
|
|
13
59
|
function GetOptionOverrides(reqCtx) {
|
|
14
60
|
return reqCtx.dataAPIEntry?.options ?? {};
|
|
15
61
|
}
|
|
16
62
|
Parameters.GetOptionOverrides = GetOptionOverrides;
|
|
17
|
-
function parseFilter(searchVal) {
|
|
18
|
-
const match = searchVal.match(/([^:]+):(.*)/);
|
|
19
|
-
return match ? [match[2], match[1]] : [searchVal, "eq"];
|
|
20
|
-
}
|
|
21
|
-
function copyFilter(filter) {
|
|
22
|
-
const copies = (0, metadata_1.FilterValuesOf)(filter).map((value) => [
|
|
23
|
-
...value,
|
|
24
|
-
]);
|
|
25
|
-
return copies.length === 1 ? copies[0] : copies;
|
|
26
|
-
}
|
|
27
63
|
/**
|
|
28
64
|
* Extracts request filters and mandatory route filters, with route field
|
|
29
65
|
* keys winning. A field the request filters once (`?filter_status=open`)
|
|
@@ -36,60 +72,55 @@ var Parameters;
|
|
|
36
72
|
for (const filter of Object.keys(meta.filters)) {
|
|
37
73
|
const values = reqCtx.url.searchParams
|
|
38
74
|
.getAll(`filter_${filter}`)
|
|
39
|
-
.map(parseFilter);
|
|
75
|
+
.map(filters_1.parseFilter);
|
|
40
76
|
if (values.length > 0) {
|
|
41
77
|
result[filter] = values.length === 1 ? values[0] : values;
|
|
42
78
|
}
|
|
43
79
|
}
|
|
44
80
|
const overrides = GetOptionOverrides(reqCtx);
|
|
45
81
|
for (const [key, value] of Object.entries(overrides.filters ?? {})) {
|
|
46
|
-
result[key] = copyFilter(value);
|
|
82
|
+
result[key] = (0, filters_1.copyFilter)(value);
|
|
47
83
|
}
|
|
48
84
|
return result;
|
|
49
85
|
}
|
|
50
86
|
Parameters.ExtractFilters = ExtractFilters;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
Parameters.FromSearch = extractors.FromSearch;
|
|
88
|
+
function combinesWithOptions(extractor) {
|
|
89
|
+
return extractor === ExtractFilters;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Builds a route's parameters: the options its `WithOptions` set, then each
|
|
93
|
+
* extracted field those options leave out. An extractor of `undefined` is
|
|
94
|
+
* skipped like an absent key. {@link ExtractFilters} also runs for a route
|
|
95
|
+
* that sets `filters`, since it combines them with the request's.
|
|
96
|
+
*/
|
|
57
97
|
function ExtractGeneric(reqCtx, meta, dynamic) {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (!(key in result
|
|
62
|
-
|
|
63
|
-
if (typeof extractor === "string") {
|
|
64
|
-
if (extractor.startsWith("multi:")) {
|
|
65
|
-
const converter = converters[extractor.substring(6)];
|
|
66
|
-
result[key] = reqCtx.url.searchParams
|
|
67
|
-
.getAll(key)
|
|
68
|
-
.map((searchVal) => converter(searchVal));
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
const searchVal = reqCtx.url.searchParams.get(key);
|
|
72
|
-
if (searchVal !== null) {
|
|
73
|
-
result[key] = converters[extractor](searchVal);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
result[key] = extractor(reqCtx, meta);
|
|
79
|
-
}
|
|
98
|
+
const result = { ...GetOptionOverrides(reqCtx) };
|
|
99
|
+
for (const key in dynamic) {
|
|
100
|
+
const extractor = dynamic[key];
|
|
101
|
+
if (!extractor || (key in result && !combinesWithOptions(extractor))) {
|
|
102
|
+
continue;
|
|
80
103
|
}
|
|
104
|
+
const value = extractor(reqCtx, meta, key);
|
|
105
|
+
if (value !== undefined)
|
|
106
|
+
result[key] = value;
|
|
81
107
|
}
|
|
82
108
|
return result;
|
|
83
109
|
}
|
|
84
110
|
Parameters.ExtractGeneric = ExtractGeneric;
|
|
111
|
+
/**
|
|
112
|
+
* The sort direction a request sends. Any other text sorts ascending, as an
|
|
113
|
+
* absent direction does.
|
|
114
|
+
*/
|
|
115
|
+
const sortDirectionOf = extractors.fromSearch((text) => text === "asc" || text === "desc" ? text : undefined);
|
|
85
116
|
Parameters.List = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
|
|
86
117
|
const meta = (0, metadata_1.GetDataControllerMeta)(this);
|
|
87
118
|
const params = ExtractGeneric(context, meta, {
|
|
88
119
|
filters: ExtractFilters,
|
|
89
|
-
offset:
|
|
90
|
-
limit:
|
|
91
|
-
sortKey:
|
|
92
|
-
sortDirection:
|
|
120
|
+
offset: Parameters.FromSearch.int,
|
|
121
|
+
limit: Parameters.FromSearch.int,
|
|
122
|
+
sortKey: Parameters.FromSearch.string,
|
|
123
|
+
sortDirection: sortDirectionOf,
|
|
93
124
|
});
|
|
94
125
|
(0, interface_api_util_1.assert)(!params.sortKey || meta.fields[params.sortKey]?.sortable, 400, "Field is not sortable.");
|
|
95
126
|
params.limit = params.limit
|
|
@@ -99,9 +130,9 @@ var Parameters;
|
|
|
99
130
|
}));
|
|
100
131
|
Parameters.Get = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
|
|
101
132
|
const params = ExtractGeneric(context, (0, metadata_1.GetDataControllerMeta)(this), {
|
|
102
|
-
id:
|
|
133
|
+
id: Parameters.FromSearch.string,
|
|
103
134
|
});
|
|
104
|
-
(0, interface_api_util_1.assert)(params.id &&
|
|
135
|
+
(0, interface_api_util_1.assert)(params.id && (0, guards_1.isString)(params.id), 400, "Missing id.");
|
|
105
136
|
return params;
|
|
106
137
|
}));
|
|
107
138
|
Parameters.New = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
|
|
@@ -110,14 +141,14 @@ var Parameters;
|
|
|
110
141
|
}));
|
|
111
142
|
Parameters.Edit = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
|
|
112
143
|
const params = ExtractGeneric(context, (0, metadata_1.GetDataControllerMeta)(this), {
|
|
113
|
-
id:
|
|
144
|
+
id: Parameters.FromSearch.string,
|
|
114
145
|
});
|
|
115
|
-
(0, interface_api_util_1.assert)(params.id &&
|
|
146
|
+
(0, interface_api_util_1.assert)(params.id && (0, guards_1.isString)(params.id), 400, "Missing id.");
|
|
116
147
|
return params;
|
|
117
148
|
}));
|
|
118
149
|
Parameters.Delete = (0, decorators_1.MakeParameterAndPropertyDecorator)((target, key, param) => (0, interface_api_1.SetParameterProvider)(target, key, param, function (context) {
|
|
119
150
|
const params = ExtractGeneric(context, (0, metadata_1.GetDataControllerMeta)(this), {
|
|
120
|
-
id:
|
|
151
|
+
id: Parameters.FromSearch.strings,
|
|
121
152
|
});
|
|
122
153
|
(0, interface_api_util_1.assert)(params.id && Array.isArray(params.id) && params.id.length > 0, 400, "Missing id.");
|
|
123
154
|
return params;
|
|
@@ -127,439 +158,27 @@ var Query;
|
|
|
127
158
|
(function (Query) {
|
|
128
159
|
function GetModel(obj, meta) {
|
|
129
160
|
(0, interface_api_util_1.assert)(meta.modelKey, 500, "Missing model key.");
|
|
130
|
-
|
|
161
|
+
const model = (0, prototypes_1.fieldsOf)(obj)[meta.modelKey];
|
|
162
|
+
(0, interface_api_util_1.assert)(tables.isModelInstance(model), 500, "The controller holds no model.");
|
|
163
|
+
return model;
|
|
131
164
|
}
|
|
132
165
|
Query.GetModel = GetModel;
|
|
133
166
|
function Foreign(db, meta, query, pluck) {
|
|
134
|
-
|
|
135
|
-
if ((0, interface_database_1.IsStream)(query)) {
|
|
136
|
-
for (const [name, field] of Object.entries(meta.fields)) {
|
|
137
|
-
if (!field.foreign || (pluck && !pluck.has(name))) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const [table, tableClass, index, multi, pluckField, schemaName, targetJoined,] = field.foreign;
|
|
141
|
-
const other = targetTable(db, { table, tableClass, schemaName });
|
|
142
|
-
const hasTargetJoined = !multi && !!targetJoined?.length;
|
|
143
|
-
if (pluckField) {
|
|
144
|
-
// The join keys of the target's @Joined fields must be fetched too
|
|
145
|
-
// (so the second lookup can resolve them), but they are NOT stored in
|
|
146
|
-
// `foreign[4]`, so they get stripped from the response by
|
|
147
|
-
// `ClearInternal` afterwards.
|
|
148
|
-
const lookupPluck = hasTargetJoined
|
|
149
|
-
? Array.from(new Set([
|
|
150
|
-
...pluckField,
|
|
151
|
-
...targetJoined.map((g) => g.localKey),
|
|
152
|
-
]))
|
|
153
|
-
: pluckField;
|
|
154
|
-
query = query.lookup(other.pluck("_internal", ...lookupPluck), name, index || "_id");
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
query = query.lookup(other, name, index || "_id");
|
|
158
|
-
}
|
|
159
|
-
if (hasTargetJoined) {
|
|
160
|
-
query = applyForeignJoinedStream(db, query, name, targetJoined);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return query;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
const oldForeign = (obj) => {
|
|
167
|
-
const changedFields = {};
|
|
168
|
-
for (const [name, field] of Object.entries(meta.fields)) {
|
|
169
|
-
if (!field.foreign || (pluck && !pluck.has(name))) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const [table, tableClass, index, multi, pluckField, schemaName, targetJoined,] = field.foreign;
|
|
173
|
-
const remote = targetTable(db, { table, tableClass, schemaName });
|
|
174
|
-
if (multi) {
|
|
175
|
-
changedFields[name] = obj.key(name)
|
|
176
|
-
.default([])
|
|
177
|
-
.map((val) => {
|
|
178
|
-
let foreignObject = Get(remote, val, index);
|
|
179
|
-
if (pluckField) {
|
|
180
|
-
foreignObject = foreignObject.pluck("_internal", ...pluckField);
|
|
181
|
-
}
|
|
182
|
-
return foreignObject.default(null);
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
let foreignObject = Get(remote, obj.key(name), index);
|
|
187
|
-
if (targetJoined && targetJoined.length > 0) {
|
|
188
|
-
foreignObject = applyForeignJoinedDatum(db, foreignObject, targetJoined);
|
|
189
|
-
}
|
|
190
|
-
changedFields[name] = foreignObject.default(null);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return obj.merge(changedFields);
|
|
194
|
-
};
|
|
195
|
-
return query.do(oldForeign);
|
|
196
|
-
}
|
|
167
|
+
return foreign.Foreign(db, meta, query, pluck);
|
|
197
168
|
}
|
|
198
169
|
Query.Foreign = Foreign;
|
|
199
|
-
/**
|
|
200
|
-
* Re-materializes the `@Joined` fields of a relation target on the looked-up
|
|
201
|
-
* sub-object (Stream/list path). The base `Foreign` lookup only plucks the
|
|
202
|
-
* target's physical columns, so a joined label would come back empty; here we
|
|
203
|
-
* resolve each joined field with a second lookup keyed on the target's
|
|
204
|
-
* (already plucked) local key, then merge it back into the relation object.
|
|
205
|
-
*/
|
|
206
|
-
function applyForeignJoinedStream(db, stream, foreignName, groups) {
|
|
207
|
-
let s = stream;
|
|
208
|
-
for (const group of groups) {
|
|
209
|
-
const remoteTable = targetTable(db, group).pluck("_internal", group.remoteIndex, group.remoteField);
|
|
210
|
-
const tmpKey = `__fjoin_${foreignName}_${group.name}`;
|
|
211
|
-
s = s
|
|
212
|
-
.map((row) => row.merge({
|
|
213
|
-
[tmpKey]: row.key(foreignName).key(group.localKey),
|
|
214
|
-
}))
|
|
215
|
-
.lookup(remoteTable, tmpKey, group.remoteIndex)
|
|
216
|
-
.map((row) => row.merge({
|
|
217
|
-
// For an orphan foreign key this yields `{ [joined]: null }`; it is
|
|
218
|
-
// normalized back to a null relation in `ClearInternal` (the DSL has
|
|
219
|
-
// no value-level conditional to do it here).
|
|
220
|
-
[foreignName]: row.key(foreignName).merge({
|
|
221
|
-
[group.name]: row
|
|
222
|
-
.key(tmpKey)
|
|
223
|
-
.default({})
|
|
224
|
-
.key(group.remoteField)
|
|
225
|
-
.default(null),
|
|
226
|
-
}),
|
|
227
|
-
}))
|
|
228
|
-
.without(tmpKey);
|
|
229
|
-
}
|
|
230
|
-
return s;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Datum/get counterpart of {@link applyForeignJoinedStream}: resolves each of
|
|
234
|
-
* the target's `@Joined` fields with a secondary `Get` and merges it into the
|
|
235
|
-
* relation object.
|
|
236
|
-
*/
|
|
237
|
-
function applyForeignJoinedDatum(db, datum, groups) {
|
|
238
|
-
let d = datum;
|
|
239
|
-
for (const group of groups) {
|
|
240
|
-
// Use the raw table here (not a plucked stream): `Get` relies on
|
|
241
|
-
// `getAll`/`get`, which exist on Selection/Table but not on a Stream.
|
|
242
|
-
const remoteTable = targetTable(db, group);
|
|
243
|
-
d = d.do((row) => row.merge({
|
|
244
|
-
[group.name]: Get(remoteTable, row.key(group.localKey), group.remoteIndex)
|
|
245
|
-
.key(group.remoteField)
|
|
246
|
-
.default(null),
|
|
247
|
-
}));
|
|
248
|
-
}
|
|
249
|
-
return d;
|
|
250
|
-
}
|
|
251
|
-
function collectJoinedGroups(meta) {
|
|
252
|
-
const groups = new Map();
|
|
253
|
-
for (const [name, field] of Object.entries(meta.fields)) {
|
|
254
|
-
if (!field.joined)
|
|
255
|
-
continue;
|
|
256
|
-
const j = field.joined;
|
|
257
|
-
const groupKey = `${j.schemaName ?? ""}|${j.table}|${j.localKey}|${j.remoteIndex}`;
|
|
258
|
-
let group = groups.get(groupKey);
|
|
259
|
-
if (!group) {
|
|
260
|
-
group = {
|
|
261
|
-
table: j.table,
|
|
262
|
-
tableClass: j.tableClass,
|
|
263
|
-
localKey: j.localKey,
|
|
264
|
-
remoteIndex: j.remoteIndex,
|
|
265
|
-
schemaName: j.schemaName,
|
|
266
|
-
fields: [],
|
|
267
|
-
};
|
|
268
|
-
groups.set(groupKey, group);
|
|
269
|
-
}
|
|
270
|
-
group.fields.push({ name, remoteField: j.remoteField });
|
|
271
|
-
}
|
|
272
|
-
return Array.from(groups.values());
|
|
273
|
-
}
|
|
274
|
-
function addAll(target, names) {
|
|
275
|
-
for (const name of names) {
|
|
276
|
-
target.add(name);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Splits the joined fields of `meta` between those {@link List} must
|
|
281
|
-
* materialize before the filter and count (`filter`) and those it only
|
|
282
|
-
* needs after the count (`sort`). Joined fields referenced by neither are
|
|
283
|
-
* left out so the list route can defer their lookups to the returned page.
|
|
284
|
-
*
|
|
285
|
-
* Computed expressions may read joined fields, so materializing a computed
|
|
286
|
-
* field pulls every remaining joined group in with it. Groups whose local
|
|
287
|
-
* key is also a foreign field must resolve before `Foreign` replaces the
|
|
288
|
-
* key with the looked-up record.
|
|
289
|
-
*/
|
|
290
|
-
function splitJoinedFields(meta, sorting, filters) {
|
|
291
|
-
const split = { filter: new Set(), sort: new Set() };
|
|
292
|
-
const filterNames = Object.keys(meta.filters).filter((name) => filters && name in filters);
|
|
293
|
-
const hasComputedFilter = filterNames.some((name) => meta.fields[name]?.computed);
|
|
294
|
-
const sortField = sorting?.[0];
|
|
295
|
-
const hasComputedSort = !!sortField && !!meta.fields[sortField]?.computed;
|
|
296
|
-
for (const group of collectJoinedGroups(meta)) {
|
|
297
|
-
const names = group.fields.map((field) => field.name);
|
|
298
|
-
if (hasComputedFilter ||
|
|
299
|
-
names.some((name) => filterNames.includes(name))) {
|
|
300
|
-
addAll(split.filter, names);
|
|
301
|
-
}
|
|
302
|
-
else if (hasComputedSort ||
|
|
303
|
-
(sortField && names.includes(sortField)) ||
|
|
304
|
-
meta.fields[group.localKey]?.foreign) {
|
|
305
|
-
addAll(split.sort, names);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return split;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Joined field names that {@link List} does not materialize for the given
|
|
312
|
-
* split. The default list route resolves these after pagination so their
|
|
313
|
-
* lookups only run on the returned page.
|
|
314
|
-
*/
|
|
315
|
-
function deferredJoinedFields(meta, split) {
|
|
316
|
-
return new Set(Object.entries(meta.fields)
|
|
317
|
-
.filter(([name, field]) => field.joined && !split.filter.has(name) && !split.sort.has(name))
|
|
318
|
-
.map(([name]) => name));
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Stream returned by {@link List} when the deferred joined fields are not
|
|
322
|
-
* exposed to the caller: it resolves them itself, so consumers that
|
|
323
|
-
* destructure `[query, total]` alone still observe every `@Joined` field.
|
|
324
|
-
* The lookups are appended after `slice`/`nth`, keeping them on the
|
|
325
|
-
* requested page only; bare `count()` calls and field aggregates on
|
|
326
|
-
* non-joined fields skip them (left-join lookups never change the row
|
|
327
|
-
* count nor other fields); `changes()` attaches to the raw stream, since
|
|
328
|
-
* change feeds cannot carry the lookups. Any other operation may observe
|
|
329
|
-
* the joined fields, so it materializes them first.
|
|
330
|
-
*/
|
|
331
|
-
class DeferredJoinedStream extends interface_database_1.Stream {
|
|
332
|
-
}
|
|
333
|
-
function materializeDeferredJoin(stream) {
|
|
334
|
-
const { db, meta, names, raw } = stream.deferredJoin;
|
|
335
|
-
return Joined(db, meta, raw, names);
|
|
336
|
-
}
|
|
337
|
-
function pagedDeferredJoinOperation(name) {
|
|
338
|
-
return function (...args) {
|
|
339
|
-
const { db, meta, names, raw } = this.deferredJoin;
|
|
340
|
-
const paged = raw[name](...args);
|
|
341
|
-
return Joined(db, meta, paged, names);
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Resolves the joined fields through the stream form of {@link Joined}
|
|
346
|
-
* rather than its datum form: only the datum form has no way to drop the
|
|
347
|
-
* temporary key it stages (neither `Datum` nor `ValueProxy` exposes
|
|
348
|
-
* `without`), so it would leave `__joined_orig_*` on the row.
|
|
349
|
-
*/
|
|
350
|
-
function nthDeferredJoinOperation() {
|
|
351
|
-
return function (n) {
|
|
352
|
-
const { db, meta, names, raw } = this.deferredJoin;
|
|
353
|
-
return Joined(db, meta, raw.slice(n, 1), names).nth(0);
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
function rawDeferredJoinOperation(name) {
|
|
357
|
-
return function (...args) {
|
|
358
|
-
return this.deferredJoin.raw[name](...args);
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
function fieldAggregateDeferredJoinOperation(name, bareCallOnRaw) {
|
|
362
|
-
return function (field) {
|
|
363
|
-
const { db, meta, names, raw } = this.deferredJoin;
|
|
364
|
-
if (!field) {
|
|
365
|
-
const bareTarget = bareCallOnRaw ? raw : materializeDeferredJoin(this);
|
|
366
|
-
return bareTarget[name]();
|
|
367
|
-
}
|
|
368
|
-
const target = names.has(field)
|
|
369
|
-
? Joined(db, meta, raw, new Set([field]))
|
|
370
|
-
: raw;
|
|
371
|
-
return target[name](field);
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
function forwardingDeferredJoinOperation(name) {
|
|
375
|
-
return function (...args) {
|
|
376
|
-
return materializeDeferredJoin(this)[name](...args);
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
const DEFERRED_JOIN_OPERATION_FACTORIES = {
|
|
380
|
-
slice: pagedDeferredJoinOperation,
|
|
381
|
-
nth: nthDeferredJoinOperation,
|
|
382
|
-
changes: rawDeferredJoinOperation,
|
|
383
|
-
count: (name) => fieldAggregateDeferredJoinOperation(name, true),
|
|
384
|
-
sum: (name) => fieldAggregateDeferredJoinOperation(name, false),
|
|
385
|
-
avg: (name) => fieldAggregateDeferredJoinOperation(name, false),
|
|
386
|
-
min: (name) => fieldAggregateDeferredJoinOperation(name, false),
|
|
387
|
-
max: (name) => fieldAggregateDeferredJoinOperation(name, false),
|
|
388
|
-
distinct: (name) => fieldAggregateDeferredJoinOperation(name, false),
|
|
389
|
-
};
|
|
390
|
-
const DEFERRED_JOIN_INHERITED_OPERATIONS = new Set(["constructor", "cast"]);
|
|
391
|
-
function wireDeferredJoinedStream() {
|
|
392
|
-
const operationNames = [
|
|
393
|
-
...Object.getOwnPropertyNames(interface_database_1.Stream.prototype),
|
|
394
|
-
"run",
|
|
395
|
-
"cursor",
|
|
396
|
-
"build",
|
|
397
|
-
].filter((name) => !DEFERRED_JOIN_INHERITED_OPERATIONS.has(name));
|
|
398
|
-
for (const name of operationNames) {
|
|
399
|
-
const factory = DEFERRED_JOIN_OPERATION_FACTORIES[name] ??
|
|
400
|
-
forwardingDeferredJoinOperation;
|
|
401
|
-
DeferredJoinedStream.prototype[name] = factory(name);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
wireDeferredJoinedStream();
|
|
405
|
-
/**
|
|
406
|
-
* Wraps `raw` into a {@link DeferredJoinedStream}. The wrapper keeps the
|
|
407
|
-
* raw stages as its own (an unobservable fallback, since every operation
|
|
408
|
-
* is overridden) while `build()` serializes the materialized pipeline, so
|
|
409
|
-
* a wrapper embedded as an argument of another query never leaks rows
|
|
410
|
-
* without the joined fields.
|
|
411
|
-
*/
|
|
412
|
-
function wrapDeferredJoined(db, meta, names, raw) {
|
|
413
|
-
const wrapped = Object.create(DeferredJoinedStream.prototype);
|
|
414
|
-
wrapped.stages = raw.build();
|
|
415
|
-
wrapped.deferredJoin = { db, meta, names, raw };
|
|
416
|
-
return wrapped;
|
|
417
|
-
}
|
|
418
|
-
function listResultTuple(tmpRequest, total, meta, joinedSplit, db, options) {
|
|
419
|
-
const deferredJoined = deferredJoinedFields(meta, joinedSplit);
|
|
420
|
-
if (options?.exposeDeferredJoined || !db || deferredJoined.size === 0) {
|
|
421
|
-
return [tmpRequest, total, deferredJoined];
|
|
422
|
-
}
|
|
423
|
-
return [
|
|
424
|
-
wrapDeferredJoined(db, meta, deferredJoined, tmpRequest),
|
|
425
|
-
total,
|
|
426
|
-
new Set(),
|
|
427
|
-
];
|
|
428
|
-
}
|
|
429
|
-
function targetTable(sourceDb, target) {
|
|
430
|
-
if (!target.schemaName)
|
|
431
|
-
return sourceDb.table(target.table);
|
|
432
|
-
(0, interface_api_util_1.assert)(target.tableClass, 500, `Table "${target.table}" of schema "${target.schemaName}" has no registered table class`);
|
|
433
|
-
return target.tableClass.table();
|
|
434
|
-
}
|
|
435
170
|
function Joined(db, meta, query, only) {
|
|
436
|
-
|
|
437
|
-
if (groups.length === 0) {
|
|
438
|
-
return query;
|
|
439
|
-
}
|
|
440
|
-
const emptyRemote = {};
|
|
441
|
-
const tmpKeyFor = (localKey) => `__joined_orig_${localKey}`;
|
|
442
|
-
if ((0, interface_database_1.IsStream)(query)) {
|
|
443
|
-
let stream = query;
|
|
444
|
-
const tmpKeys = [];
|
|
445
|
-
for (const group of groups) {
|
|
446
|
-
const remoteFields = group.fields.map((f) => f.remoteField);
|
|
447
|
-
const remoteTable = targetTable(db, group).pluck("_internal", group.remoteIndex, ...remoteFields);
|
|
448
|
-
const tmpKey = tmpKeyFor(group.localKey);
|
|
449
|
-
tmpKeys.push(tmpKey);
|
|
450
|
-
stream = stream
|
|
451
|
-
.map((row) => row.merge({ [tmpKey]: row.key(group.localKey) }))
|
|
452
|
-
.lookup(remoteTable, group.localKey, group.remoteIndex)
|
|
453
|
-
.map((row) => {
|
|
454
|
-
const merged = {};
|
|
455
|
-
const remote = row.key(group.localKey).default(emptyRemote);
|
|
456
|
-
for (const f of group.fields) {
|
|
457
|
-
merged[f.name] = remote.key(f.remoteField).default(null);
|
|
458
|
-
}
|
|
459
|
-
merged[group.localKey] = row.key(tmpKey);
|
|
460
|
-
return row.merge(merged);
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
return stream.without(...tmpKeys);
|
|
464
|
-
}
|
|
465
|
-
let datum = query;
|
|
466
|
-
for (const group of groups) {
|
|
467
|
-
const remoteFields = group.fields.map((f) => f.remoteField);
|
|
468
|
-
const remoteTable = targetTable(db, group).pluck("_internal", group.remoteIndex, ...remoteFields);
|
|
469
|
-
const tmpKey = tmpKeyFor(group.localKey);
|
|
470
|
-
datum = datum
|
|
471
|
-
.do((row) => row.merge({ [tmpKey]: row.key(group.localKey) }))
|
|
472
|
-
.lookup(remoteTable, group.localKey, group.remoteIndex)
|
|
473
|
-
.do((row) => {
|
|
474
|
-
const merged = {};
|
|
475
|
-
const remote = row.key(group.localKey).default(emptyRemote);
|
|
476
|
-
for (const f of group.fields) {
|
|
477
|
-
merged[f.name] = remote.key(f.remoteField).default(null);
|
|
478
|
-
}
|
|
479
|
-
merged[group.localKey] = row.key(tmpKey);
|
|
480
|
-
return row.merge(merged);
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
return datum;
|
|
171
|
+
return joined.Joined(db, meta, query, only);
|
|
484
172
|
}
|
|
485
173
|
Query.Joined = Joined;
|
|
486
|
-
function collectComputedEntries(meta, only) {
|
|
487
|
-
return Object.entries(meta.fields)
|
|
488
|
-
.filter(([name, field]) => field.computed && (!only || only.has(name)))
|
|
489
|
-
.map(([name, field]) => [name, field.computed]);
|
|
490
|
-
}
|
|
491
|
-
function buildComputedMappers(db, entries) {
|
|
492
|
-
const mergeExpressions = (row) => {
|
|
493
|
-
const merged = {};
|
|
494
|
-
for (const [name, computed] of entries) {
|
|
495
|
-
merged[name] = computed.expr(row, db);
|
|
496
|
-
}
|
|
497
|
-
return row.merge(merged);
|
|
498
|
-
};
|
|
499
|
-
const defaulted = entries.filter(([, computed]) => computed.default !== undefined);
|
|
500
|
-
if (defaulted.length === 0) {
|
|
501
|
-
return [mergeExpressions];
|
|
502
|
-
}
|
|
503
|
-
const applyDefaults = (row) => {
|
|
504
|
-
const merged = {};
|
|
505
|
-
for (const [name, computed] of defaulted) {
|
|
506
|
-
merged[name] = row.key(name).default(computed.default);
|
|
507
|
-
}
|
|
508
|
-
return row.merge(merged);
|
|
509
|
-
};
|
|
510
|
-
return [mergeExpressions, applyDefaults];
|
|
511
|
-
}
|
|
512
174
|
function Computed(db, meta, query, only) {
|
|
513
|
-
|
|
514
|
-
if (entries.length === 0) {
|
|
515
|
-
return query;
|
|
516
|
-
}
|
|
517
|
-
const mappers = buildComputedMappers(db, entries);
|
|
518
|
-
if ((0, interface_database_1.IsStream)(query)) {
|
|
519
|
-
return mappers.reduce((stream, mapper) => stream.map(mapper), query);
|
|
520
|
-
}
|
|
521
|
-
return mappers.reduce((datum, mapper) => datum.do(mapper), query);
|
|
175
|
+
return computed.Computed(db, meta, query, only);
|
|
522
176
|
}
|
|
523
177
|
Query.Computed = Computed;
|
|
524
178
|
async function ReadProperties(obj, meta, dbData, action, onlyList) {
|
|
525
|
-
|
|
526
|
-
const instance = { ...obj };
|
|
527
|
-
const res = {};
|
|
528
|
-
for (const [key, field] of readable.props) {
|
|
529
|
-
if (onlyList && !field.listable) {
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
instance[key] = dbData[field.dbName || key];
|
|
533
|
-
res[key] = dbData[field.dbName || key];
|
|
534
|
-
}
|
|
535
|
-
instance.table = dbData;
|
|
536
|
-
Object.setPrototypeOf(instance, meta.target.prototype);
|
|
537
|
-
for (const [key, field] of readable.getters) {
|
|
538
|
-
if (onlyList && !field.listable) {
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
const val = field.desc?.get?.apply(instance);
|
|
542
|
-
res[key] = await (typeof val === "function" ? val() : val);
|
|
543
|
-
}
|
|
544
|
-
return res;
|
|
179
|
+
return (0, read_properties_1.ReadProperties)(obj, meta, dbData, action, onlyList);
|
|
545
180
|
}
|
|
546
181
|
Query.ReadProperties = ReadProperties;
|
|
547
|
-
function writableInBody(writable, bodyData) {
|
|
548
|
-
return {
|
|
549
|
-
props: writable.props.filter(([key]) => Object.hasOwn(bodyData, key)),
|
|
550
|
-
setters: writable.setters.filter(([key]) => Object.hasOwn(bodyData, key)),
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
function writeTargetDefaults(instance, meta, dbData) {
|
|
554
|
-
for (const [key, value] of Object.entries(new meta.target())) {
|
|
555
|
-
if (value !== undefined) {
|
|
556
|
-
instance[key] = value;
|
|
557
|
-
if (key in meta.fields) {
|
|
558
|
-
dbData[meta.fields[key].dbName || key] = value;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
182
|
/**
|
|
564
183
|
* Writes the writable fields of `bodyData` into a database row.
|
|
565
184
|
*
|
|
@@ -571,45 +190,13 @@ var Query;
|
|
|
571
190
|
* `null` clears it.
|
|
572
191
|
*/
|
|
573
192
|
async function WriteProperties(obj, meta, bodyData, action, existingDBData) {
|
|
574
|
-
|
|
575
|
-
const writable = existingDBData
|
|
576
|
-
? writableInBody(actionWritable, bodyData)
|
|
577
|
-
: actionWritable;
|
|
578
|
-
const instance = { ...obj };
|
|
579
|
-
const dbData = existingDBData || {};
|
|
580
|
-
Object.setPrototypeOf(dbData, meta.tableClass.prototype);
|
|
581
|
-
if (!existingDBData) {
|
|
582
|
-
writeTargetDefaults(instance, meta, dbData);
|
|
583
|
-
}
|
|
584
|
-
for (const [key, field] of writable.props) {
|
|
585
|
-
instance[key] = bodyData[key];
|
|
586
|
-
dbData[field.dbName || key] = bodyData[key];
|
|
587
|
-
}
|
|
588
|
-
instance.table = dbData;
|
|
589
|
-
Object.setPrototypeOf(instance, meta.target.prototype);
|
|
590
|
-
for (const [key, field] of writable.setters) {
|
|
591
|
-
field.desc?.set?.apply(instance, [bodyData[key]]);
|
|
592
|
-
}
|
|
593
|
-
return dbData;
|
|
193
|
+
return properties.WriteProperties(obj, meta, bodyData, action, existingDBData);
|
|
594
194
|
}
|
|
595
195
|
Query.WriteProperties = WriteProperties;
|
|
596
196
|
function Get(table, id, index) {
|
|
597
|
-
return index
|
|
598
|
-
? table.getAll(id, index).nth(0)
|
|
599
|
-
: table.get(id);
|
|
197
|
+
return tables.Get(table, id, index);
|
|
600
198
|
}
|
|
601
199
|
Query.Get = Get;
|
|
602
|
-
/** Where a list sorted on a modifier-held field keeps the value it sorts on. */
|
|
603
|
-
const MODIFIED_SORT_KEY = "__dataApiSortValue";
|
|
604
|
-
/**
|
|
605
|
-
* Whether a field is held by a modifier the controller keys (a localized
|
|
606
|
-
* field read in the request's locale): stored under the modifier, it sorts
|
|
607
|
-
* on the value the request reads, not on the stored field.
|
|
608
|
-
*/
|
|
609
|
-
function isHeldByKeyedModifier(meta, field) {
|
|
610
|
-
const keyed = new Set(Array.from(meta.modifierKeys.keys(), (modifier) => modifier.name));
|
|
611
|
-
return ((0, common_1.getMetadata)(meta.tableClass, common_2.ModifiersStaticMetadata).fields[field]?.some(({ id }) => keyed.has(id)) ?? false);
|
|
612
|
-
}
|
|
613
200
|
/**
|
|
614
201
|
* Builds the filtered and sorted stream of a list request along with its
|
|
615
202
|
* total count. Joined fields that no filter or sort references are not
|
|
@@ -620,93 +207,15 @@ var Query;
|
|
|
620
207
|
* since no lookup can be built without one — the raw stream is returned
|
|
621
208
|
* and the third element names the joined fields left for the caller.
|
|
622
209
|
*/
|
|
623
|
-
function List(obj, meta, request, reqCtx, sorting, filters, db, options) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
values[0][1] === "eq" &&
|
|
631
|
-
meta.fields[name]?.indexable);
|
|
632
|
-
})?.[0];
|
|
633
|
-
const index = indexFilter
|
|
634
|
-
? meta.fields[indexFilter].dbName || indexFilter
|
|
635
|
-
: undefined;
|
|
636
|
-
const indexedFilter = indexFilter
|
|
637
|
-
? (0, metadata_1.FilterValuesOf)(filters?.[indexFilter] ?? [])[0]
|
|
638
|
-
: undefined;
|
|
639
|
-
let tmpRequest = index
|
|
640
|
-
? request.getAll(indexedFilter?.[0] ?? "", index)
|
|
641
|
-
: request;
|
|
642
|
-
const joinedSplit = splitJoinedFields(meta, sorting, filters);
|
|
643
|
-
if (db && joinedSplit.filter.size > 0) {
|
|
644
|
-
tmpRequest = Joined(db, meta, tmpRequest, joinedSplit.filter);
|
|
645
|
-
}
|
|
646
|
-
const filteredComputed = new Set(filterList
|
|
647
|
-
.map(([name]) => name)
|
|
648
|
-
.filter((name) => meta.fields[name]?.computed));
|
|
649
|
-
if (db && filteredComputed.size > 0) {
|
|
650
|
-
tmpRequest = Computed(db, meta, tmpRequest, filteredComputed);
|
|
651
|
-
}
|
|
652
|
-
const sortField = sorting?.[0];
|
|
653
|
-
const shouldSort = sortField ? meta.fields[sortField]?.sortable : undefined;
|
|
654
|
-
const isModifiedSort = !!shouldSort && !!sortField && isHeldByKeyedModifier(meta, sortField);
|
|
655
|
-
if (shouldSort?.indexed && sortField && !isModifiedSort) {
|
|
656
|
-
tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc");
|
|
657
|
-
}
|
|
658
|
-
if (filterList.length > 0) {
|
|
659
|
-
// TODO: rework for modifier-affected fields (unlockrequest)
|
|
660
|
-
tmpRequest = filterList.reduce((req, [name, filter]) => {
|
|
661
|
-
const fieldFilter = filters?.[name];
|
|
662
|
-
if (!fieldFilter || name === indexFilter)
|
|
663
|
-
return req;
|
|
664
|
-
return (0, metadata_1.FilterValuesOf)(fieldFilter).reduce((filtered, [value, mode]) => filtered.filter((row) => filter(Object.assign(reqCtx, { this: obj }), Validation.UnlockRequest(obj, meta, row, name), name, value, mode, row)), req);
|
|
665
|
-
}, tmpRequest);
|
|
666
|
-
}
|
|
667
|
-
const total = tmpRequest.count();
|
|
668
|
-
if (db && joinedSplit.sort.size > 0) {
|
|
669
|
-
tmpRequest = Joined(db, meta, tmpRequest, joinedSplit.sort);
|
|
670
|
-
}
|
|
671
|
-
if (db &&
|
|
672
|
-
sortField &&
|
|
673
|
-
meta.fields[sortField]?.computed &&
|
|
674
|
-
!filteredComputed.has(sortField)) {
|
|
675
|
-
tmpRequest = Computed(db, meta, tmpRequest, new Set([sortField]));
|
|
676
|
-
}
|
|
677
|
-
if (isModifiedSort && sortField) {
|
|
678
|
-
// Sorts on the value in the request's locale, else its fallback.
|
|
679
|
-
tmpRequest = tmpRequest
|
|
680
|
-
.map((row) => row.merge({
|
|
681
|
-
[MODIFIED_SORT_KEY]: Validation.UnlockRequest(obj, meta, row, sortField),
|
|
682
|
-
}))
|
|
683
|
-
.orderBy(MODIFIED_SORT_KEY, sorting?.[1] ?? "asc")
|
|
684
|
-
.without(MODIFIED_SORT_KEY);
|
|
685
|
-
}
|
|
686
|
-
else if (shouldSort && !shouldSort.indexed && sortField) {
|
|
687
|
-
tmpRequest = tmpRequest.orderBy(sortField, sorting?.[1] ?? "asc");
|
|
688
|
-
}
|
|
689
|
-
return listResultTuple(tmpRequest, total, meta, joinedSplit, db, options);
|
|
210
|
+
function List(obj, meta, request, reqCtx, ...[sorting, filters, db, options]) {
|
|
211
|
+
return list.List(obj, meta, request, reqCtx, {
|
|
212
|
+
sorting,
|
|
213
|
+
filters,
|
|
214
|
+
db,
|
|
215
|
+
options,
|
|
216
|
+
});
|
|
690
217
|
}
|
|
691
218
|
Query.List = List;
|
|
692
|
-
function pageComputedFields(meta, options) {
|
|
693
|
-
const merged = new Set(Object.keys(options.filters ?? {}).filter((name) => name in meta.filters));
|
|
694
|
-
if (options.sortKey)
|
|
695
|
-
merged.add(options.sortKey);
|
|
696
|
-
const names = Object.entries(meta.fields)
|
|
697
|
-
.filter(([name, field]) => field.computed &&
|
|
698
|
-
!merged.has(name) &&
|
|
699
|
-
(!options.pluck || options.pluck.has(name)))
|
|
700
|
-
.map(([name]) => name);
|
|
701
|
-
return new Set(names);
|
|
702
|
-
}
|
|
703
|
-
function pageJoinedFields(options, materializeAll) {
|
|
704
|
-
const deferred = options.deferredJoined ?? new Set();
|
|
705
|
-
if (materializeAll || !options.pluck)
|
|
706
|
-
return deferred;
|
|
707
|
-
const pluck = options.pluck;
|
|
708
|
-
return new Set(Array.from(deferred).filter((name) => pluck.has(name)));
|
|
709
|
-
}
|
|
710
219
|
/**
|
|
711
220
|
* One page of the stream {@link List} built, with the fields it answers:
|
|
712
221
|
* the deferred joined fields, then the computed fields no filter or sort
|
|
@@ -716,19 +225,7 @@ var Query;
|
|
|
716
225
|
* it.
|
|
717
226
|
*/
|
|
718
227
|
function ListPage(db, meta, query, options) {
|
|
719
|
-
|
|
720
|
-
const computed = pageComputedFields(meta, options);
|
|
721
|
-
// A computed expression may read any joined field, so when one is
|
|
722
|
-
// materialized every deferred joined group is merged first, whatever
|
|
723
|
-
// the pluck; the final pluck strips what the page did not ask for.
|
|
724
|
-
const joined = pageJoinedFields(options, computed.size > 0);
|
|
725
|
-
if (joined.size > 0)
|
|
726
|
-
page = Joined(db, meta, page, joined);
|
|
727
|
-
if (computed.size > 0)
|
|
728
|
-
page = Computed(db, meta, page, computed);
|
|
729
|
-
if (!options.noForeign)
|
|
730
|
-
page = Foreign(db, meta, page, options.pluck);
|
|
731
|
-
return options.pluck ? page.pluck("_internal", ...options.pluck) : page;
|
|
228
|
+
return list.ListPage(db, meta, query, options);
|
|
732
229
|
}
|
|
733
230
|
Query.ListPage = ListPage;
|
|
734
231
|
function Delete(table, id) {
|
|
@@ -743,8 +240,8 @@ var Validation;
|
|
|
743
240
|
function isClearingValue(value) {
|
|
744
241
|
if (value === null || value === undefined)
|
|
745
242
|
return true;
|
|
746
|
-
if (
|
|
747
|
-
return
|
|
243
|
+
if (value === "")
|
|
244
|
+
return true;
|
|
748
245
|
return Array.isArray(value) && value.length === 0;
|
|
749
246
|
}
|
|
750
247
|
const isAbsentFromBody = (body, name) => !(name in body);
|
|
@@ -777,7 +274,7 @@ var Validation;
|
|
|
777
274
|
}
|
|
778
275
|
Validation.MandatoryFields = MandatoryFields;
|
|
779
276
|
function toParseResult(result, value) {
|
|
780
|
-
if (
|
|
277
|
+
if (result !== true && result !== false)
|
|
781
278
|
return result;
|
|
782
279
|
return result ? { success: true, data: value } : { success: false };
|
|
783
280
|
}
|
|
@@ -810,17 +307,7 @@ var Validation;
|
|
|
810
307
|
* body already parsed is checked, not parsed again.
|
|
811
308
|
*/
|
|
812
309
|
function ParseBody(body) {
|
|
813
|
-
|
|
814
|
-
if (Buffer.isBuffer(body)) {
|
|
815
|
-
try {
|
|
816
|
-
data = JSON.parse(body.toString());
|
|
817
|
-
}
|
|
818
|
-
catch {
|
|
819
|
-
(0, interface_api_util_1.assert)(false, 400, "Invalid JSON body.");
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
(0, interface_api_util_1.assert)(typeof data === "object" && data !== null && !Array.isArray(data), 400, "The request body must be a JSON object.");
|
|
823
|
-
return data;
|
|
310
|
+
return validation.ParseBody(body);
|
|
824
311
|
}
|
|
825
312
|
Validation.ParseBody = ParseBody;
|
|
826
313
|
/**
|
|
@@ -831,85 +318,19 @@ var Validation;
|
|
|
831
318
|
*/
|
|
832
319
|
function Lock(obj, meta, data) {
|
|
833
320
|
const written = Object.keys((0, common_1.getMetadata)(data, common_2.ModifiersDynamicMetadata).floating);
|
|
834
|
-
|
|
835
|
-
const key = obj[field];
|
|
836
|
-
(0, common_2.lock)(data, modifier, written, key);
|
|
837
|
-
}
|
|
321
|
+
validation.Lock(obj, meta, data, written);
|
|
838
322
|
}
|
|
839
323
|
Validation.Lock = Lock;
|
|
840
324
|
function Unlock(obj, meta, dbData) {
|
|
841
|
-
|
|
842
|
-
if (field.foreign &&
|
|
843
|
-
dbData[name] &&
|
|
844
|
-
typeof dbData[name] === "object" &&
|
|
845
|
-
field.foreign[1]) {
|
|
846
|
-
const foreignSchema = field.foreign[1];
|
|
847
|
-
dbData[name] = Array.isArray(dbData[name])
|
|
848
|
-
? dbData[name].map((entry) => (0, common_2.fromDatabase)(entry, foreignSchema))
|
|
849
|
-
: (0, common_2.fromDatabase)(dbData[name], field.foreign[1]);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
for (const [modifier, field] of meta.modifierKeys.entries()) {
|
|
853
|
-
const key = obj[field];
|
|
854
|
-
(0, common_2.unlock)(dbData, modifier, undefined, key);
|
|
855
|
-
for (const [foreign, fieldData] of Object.entries(meta.fields)) {
|
|
856
|
-
if (fieldData.foreign &&
|
|
857
|
-
dbData[foreign] &&
|
|
858
|
-
typeof dbData[foreign] === "object") {
|
|
859
|
-
(0, common_2.unlock)(dbData[foreign], modifier, undefined, key);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
325
|
+
validation.Unlock(obj, meta, dbData);
|
|
863
326
|
}
|
|
864
327
|
Validation.Unlock = Unlock;
|
|
865
328
|
function UnlockRequest(obj, meta, row, field) {
|
|
866
|
-
|
|
867
|
-
modifier,
|
|
868
|
-
args: [obj[field]],
|
|
869
|
-
}));
|
|
870
|
-
return (0, common_2.unlockrequest)(meta.tableClass, row, field, modifiers);
|
|
329
|
+
return validation.UnlockRequest(obj, meta, row, field);
|
|
871
330
|
}
|
|
872
331
|
Validation.UnlockRequest = UnlockRequest;
|
|
873
332
|
function ClearInternal(meta, obj) {
|
|
874
|
-
|
|
875
|
-
const foreignFields = Object.entries(meta.fields).filter(([, field]) => field.foreign);
|
|
876
|
-
for (const entry of results) {
|
|
877
|
-
delete entry._internal;
|
|
878
|
-
for (const [name, { foreign }] of foreignFields) {
|
|
879
|
-
if (foreign &&
|
|
880
|
-
entry[name] &&
|
|
881
|
-
typeof entry[name] === "object" &&
|
|
882
|
-
(!Array.isArray(entry[name]) || typeof entry[name][0] === "object")) {
|
|
883
|
-
const processForeign = (data) => {
|
|
884
|
-
const plain = (0, common_2.toPlainData)(data);
|
|
885
|
-
if (!foreign?.[4]) {
|
|
886
|
-
return plain;
|
|
887
|
-
}
|
|
888
|
-
// An orphan foreign key whose target exposes @Joined fields produces
|
|
889
|
-
// an object with only those joined fields (all null) and none of the
|
|
890
|
-
// physical pluck fields. Normalize it back to a null relation, so it
|
|
891
|
-
// matches the get path and non-joined foreigns (which already null).
|
|
892
|
-
const targetJoined = foreign[6];
|
|
893
|
-
if (targetJoined?.length) {
|
|
894
|
-
const joinedNames = new Set(targetJoined.map((g) => g.name));
|
|
895
|
-
const physicalKeys = foreign[4].filter((k) => !joinedNames.has(k));
|
|
896
|
-
if (physicalKeys.length > 0 &&
|
|
897
|
-
physicalKeys.every((k) => plain[k] === null || plain[k] === undefined)) {
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const plainPlucked = {};
|
|
902
|
-
for (const key of foreign[4]) {
|
|
903
|
-
plainPlucked[key] = plain[key];
|
|
904
|
-
}
|
|
905
|
-
return plainPlucked;
|
|
906
|
-
};
|
|
907
|
-
entry[name] = Array.isArray(entry[name])
|
|
908
|
-
? entry[name].map(processForeign)
|
|
909
|
-
: processForeign(entry[name]);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
333
|
+
validation.ClearInternal(meta, obj);
|
|
913
334
|
}
|
|
914
335
|
Validation.ClearInternal = ClearInternal;
|
|
915
336
|
})(Validation || (exports.Validation = Validation = {}));
|