@eleven-am/golem-core 0.1.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/LICENSE +674 -0
- package/README.md +5 -0
- package/dist/authorization.d.ts +16 -0
- package/dist/authorization.js +33 -0
- package/dist/concurrency.d.ts +2 -0
- package/dist/concurrency.js +16 -0
- package/dist/datamodel.d.ts +48 -0
- package/dist/datamodel.js +2 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +42 -0
- package/dist/event-buffer.d.ts +5 -0
- package/dist/event-buffer.js +22 -0
- package/dist/events.d.ts +13 -0
- package/dist/events.js +6 -0
- package/dist/extensions.d.ts +17 -0
- package/dist/extensions.js +12 -0
- package/dist/hooks.d.ts +18 -0
- package/dist/hooks.js +38 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +32 -0
- package/dist/inputs.d.ts +29 -0
- package/dist/inputs.js +172 -0
- package/dist/model-meta.d.ts +10 -0
- package/dist/model-meta.js +37 -0
- package/dist/naming.d.ts +10 -0
- package/dist/naming.js +50 -0
- package/dist/nested-writes.d.ts +24 -0
- package/dist/nested-writes.js +193 -0
- package/dist/operations.d.ts +122 -0
- package/dist/operations.js +484 -0
- package/dist/publisher.d.ts +16 -0
- package/dist/publisher.js +55 -0
- package/dist/readtree.d.ts +40 -0
- package/dist/readtree.js +206 -0
- package/dist/schema.d.ts +23 -0
- package/dist/schema.js +580 -0
- package/dist/select.d.ts +15 -0
- package/dist/select.js +67 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +16 -0
- package/dist/typemap.d.ts +53 -0
- package/dist/typemap.js +2 -0
- package/dist/verify.d.ts +21 -0
- package/dist/verify.js +241 -0
- package/package.json +51 -0
package/dist/readtree.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.constraintFieldNames = constraintFieldNames;
|
|
4
|
+
exports.prepareReadTree = prepareReadTree;
|
|
5
|
+
exports.applyToOneChecks = applyToOneChecks;
|
|
6
|
+
exports.applyFieldMasks = applyFieldMasks;
|
|
7
|
+
exports.stripInjectedFields = stripInjectedFields;
|
|
8
|
+
const authorization_1 = require("./authorization");
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
function constraintFieldNames(constraint, into = new Set()) {
|
|
11
|
+
if (!constraint || typeof constraint !== 'object') {
|
|
12
|
+
return into;
|
|
13
|
+
}
|
|
14
|
+
for (const [key, value] of Object.entries(constraint)) {
|
|
15
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
16
|
+
const branches = Array.isArray(value) ? value : [value];
|
|
17
|
+
for (const branch of branches) {
|
|
18
|
+
constraintFieldNames(branch, into);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
into.add(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return into;
|
|
26
|
+
}
|
|
27
|
+
async function prepareReadTree(options) {
|
|
28
|
+
const checks = [];
|
|
29
|
+
const maskChecks = [];
|
|
30
|
+
const injected = [];
|
|
31
|
+
const classifying = options.readFieldChecks === true &&
|
|
32
|
+
options.provider?.classifyFields !== undefined &&
|
|
33
|
+
options.provider?.checkField !== undefined;
|
|
34
|
+
async function classifyModelFields(model, tree, path) {
|
|
35
|
+
if (!classifying) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const scalarNames = options.metadata?.get(model.name)?.scalarFields.map((f) => f.name) ??
|
|
39
|
+
model.fields.filter((f) => f.kind !== 'object').map((f) => f.name);
|
|
40
|
+
const requested = tree
|
|
41
|
+
? scalarNames.filter((name) => tree[name] === true)
|
|
42
|
+
: scalarNames;
|
|
43
|
+
if (requested.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const classification = await options.provider.classifyFields('read', model.name, requested, options.context);
|
|
47
|
+
for (const field of requested) {
|
|
48
|
+
const entry = classification[field];
|
|
49
|
+
if (!entry || entry.access === 'always') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (entry.access === 'never') {
|
|
53
|
+
throw new errors_1.GolemForbiddenError(`Cannot read field "${field}" on ${model.name}`);
|
|
54
|
+
}
|
|
55
|
+
maskChecks.push({ path, model: model.name, field });
|
|
56
|
+
if (tree) {
|
|
57
|
+
for (const required of entry.requires ?? []) {
|
|
58
|
+
if (tree[required] === undefined && scalarNames.includes(required)) {
|
|
59
|
+
tree[required] = true;
|
|
60
|
+
injected.push({ path, field: required });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function walkTree(model, tree, depth, path) {
|
|
67
|
+
if (!tree) {
|
|
68
|
+
return tree;
|
|
69
|
+
}
|
|
70
|
+
const rewritten = {};
|
|
71
|
+
for (const [key, entry] of Object.entries(tree)) {
|
|
72
|
+
const field = options.metadata?.get(model.name)?.fieldsByName.get(key) ??
|
|
73
|
+
model.fields.find((f) => f.name === key);
|
|
74
|
+
if (!field || field.kind !== 'object' || entry === false || entry === undefined) {
|
|
75
|
+
rewritten[key] = entry;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const target = options.modelsByName.get(field.type);
|
|
79
|
+
if (!target) {
|
|
80
|
+
rewritten[key] = entry;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (depth + 1 > options.maxDepth) {
|
|
84
|
+
throw new errors_1.GolemValidationError(`Query depth ${depth + 1} exceeds the maximum of ${options.maxDepth}`);
|
|
85
|
+
}
|
|
86
|
+
let constraint;
|
|
87
|
+
if (options.provider) {
|
|
88
|
+
constraint = await options.provider.constrain('read', target.name, options.context);
|
|
89
|
+
}
|
|
90
|
+
const conditional = (0, authorization_1.isConditionalConstraint)(constraint);
|
|
91
|
+
const relationPath = [...path, key];
|
|
92
|
+
const entryObject = entry === true && classifying
|
|
93
|
+
? {
|
|
94
|
+
select: Object.fromEntries((options.metadata?.get(target.name)?.scalarFields ??
|
|
95
|
+
target.fields.filter((candidate) => candidate.kind !== 'object'))
|
|
96
|
+
.map((candidate) => [candidate.name, true])),
|
|
97
|
+
}
|
|
98
|
+
: entry === true ? {} : { ...entry };
|
|
99
|
+
if (entryObject.select !== undefined) {
|
|
100
|
+
entryObject.select = await walkTree(target, entryObject.select, depth + 1, relationPath);
|
|
101
|
+
}
|
|
102
|
+
if (entryObject.include !== undefined) {
|
|
103
|
+
entryObject.include = await walkTree(target, entryObject.include, depth + 1, relationPath);
|
|
104
|
+
}
|
|
105
|
+
if (conditional) {
|
|
106
|
+
if (field.isList) {
|
|
107
|
+
entryObject.where = (0, authorization_1.mergeConstraint)(entryObject.where, constraint);
|
|
108
|
+
}
|
|
109
|
+
else if (options.provider?.check) {
|
|
110
|
+
if (entryObject.select) {
|
|
111
|
+
for (const name of constraintFieldNames(constraint)) {
|
|
112
|
+
const constraintField = options.metadata?.get(target.name)?.fieldsByName.get(name) ??
|
|
113
|
+
target.fields.find((f) => f.name === name);
|
|
114
|
+
if (constraintField && constraintField.kind !== 'object') {
|
|
115
|
+
entryObject.select[name] ??= true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
checks.push({ path: relationPath, model: target.name });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new errors_1.GolemForbiddenError(`Cannot traverse relation ${model.name}.${key}: row-level constraints apply and the authorization provider does not support instance checks`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
rewritten[key] = Object.keys(entryObject).length === 0 ? true : entryObject;
|
|
126
|
+
}
|
|
127
|
+
await classifyModelFields(model, rewritten, path);
|
|
128
|
+
return rewritten;
|
|
129
|
+
}
|
|
130
|
+
const select = await walkTree(options.model, options.select, 1, []);
|
|
131
|
+
const include = await walkTree(options.model, options.include, 1, []);
|
|
132
|
+
if (!options.select && !options.include) {
|
|
133
|
+
await classifyModelFields(options.model, undefined, []);
|
|
134
|
+
}
|
|
135
|
+
return { select, include, toOneChecks: checks, maskChecks, injected };
|
|
136
|
+
}
|
|
137
|
+
async function applyToOneChecks(data, checks, provider, context) {
|
|
138
|
+
for (const check of checks) {
|
|
139
|
+
await applyCheck(data, check.path, check.model, provider, context);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function applyFieldMasks(data, checks, provider, context) {
|
|
143
|
+
for (const check of checks) {
|
|
144
|
+
await walkAndTransform(data, check.path, async (row) => {
|
|
145
|
+
if (row[check.field] === undefined || row[check.field] === null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const allowed = await provider.checkField('read', check.model, row, check.field, context);
|
|
149
|
+
if (!allowed) {
|
|
150
|
+
row[check.field] = null;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function stripInjectedFields(data, injected) {
|
|
156
|
+
for (const entry of injected) {
|
|
157
|
+
await walkAndTransform(data, entry.path, async (row) => {
|
|
158
|
+
delete row[entry.field];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function walkAndTransform(node, path, transform) {
|
|
163
|
+
if (!node || typeof node !== 'object') {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(node)) {
|
|
167
|
+
for (const item of node) {
|
|
168
|
+
await walkAndTransform(item, path, transform);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (path.length === 0) {
|
|
173
|
+
await transform(node);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const [segment, ...rest] = path;
|
|
177
|
+
await walkAndTransform(node[segment], rest, transform);
|
|
178
|
+
}
|
|
179
|
+
async function applyCheck(node, path, model, provider, context) {
|
|
180
|
+
if (!node || typeof node !== 'object') {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(node)) {
|
|
184
|
+
for (const item of node) {
|
|
185
|
+
await applyCheck(item, path, model, provider, context);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const container = node;
|
|
190
|
+
const [segment, ...rest] = path;
|
|
191
|
+
const value = container[segment];
|
|
192
|
+
if (value === undefined || value === null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (rest.length > 0) {
|
|
196
|
+
await applyCheck(value, rest, model, provider, context);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const allowed = await provider.check('read', model, value, context);
|
|
203
|
+
if (!allowed) {
|
|
204
|
+
container[segment] = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GraphQLScalarType, GraphQLSchema } from 'graphql';
|
|
2
|
+
import { AuthorizationProvider } from './authorization';
|
|
3
|
+
import { GolemDefaults, DatamodelDocument, ModelsConfig } from './datamodel';
|
|
4
|
+
import { GolemEventBus } from './events';
|
|
5
|
+
import { ComputedFieldSpec, CustomOperationSpec } from './extensions';
|
|
6
|
+
import { HookRegistry } from './hooks';
|
|
7
|
+
import { GolemEngine } from './operations';
|
|
8
|
+
export declare const DateTimeScalar: GraphQLScalarType<Date | null, unknown>;
|
|
9
|
+
export interface BuildGolemSchemaOptions<TModels = Record<string, string>> {
|
|
10
|
+
datamodel: DatamodelDocument<TModels>;
|
|
11
|
+
client: Record<string, any>;
|
|
12
|
+
models?: ModelsConfig<TModels>;
|
|
13
|
+
defaults?: GolemDefaults;
|
|
14
|
+
eventBus?: GolemEventBus;
|
|
15
|
+
hooks?: HookRegistry;
|
|
16
|
+
engine?: GolemEngine;
|
|
17
|
+
computedFields?: readonly ComputedFieldSpec[];
|
|
18
|
+
customOperations?: readonly CustomOperationSpec[];
|
|
19
|
+
authorization?: AuthorizationProvider;
|
|
20
|
+
}
|
|
21
|
+
export declare function createGolemEngine<TModels>(options: BuildGolemSchemaOptions<TModels>): GolemEngine;
|
|
22
|
+
export declare function subscribableModels<TModels>(options: Pick<BuildGolemSchemaOptions<TModels>, 'datamodel' | 'models' | 'defaults'>): Set<string>;
|
|
23
|
+
export declare function buildGolemSchema<TModels>(options: BuildGolemSchemaOptions<TModels>): GraphQLSchema;
|