@tinacms/graphql 3.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/datalayer.d.ts +2 -2
- package/dist/database/index.d.ts +18 -0
- package/dist/index.js +635 -324
- package/dist/references/visit.d.ts +25 -0
- package/dist/resolve.d.ts +5 -4
- package/dist/resolver/auth-fields.d.ts +48 -7
- package/dist/resolver/index.d.ts +37 -6
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3058,7 +3058,7 @@ var assertCheckboxGroupIsList = (field) => {
|
|
|
3058
3058
|
var package_default = {
|
|
3059
3059
|
name: "@tinacms/graphql",
|
|
3060
3060
|
type: "module",
|
|
3061
|
-
version: "
|
|
3061
|
+
version: "4.0.1",
|
|
3062
3062
|
main: "dist/index.js",
|
|
3063
3063
|
module: "./dist/index.js",
|
|
3064
3064
|
files: [
|
|
@@ -3101,7 +3101,7 @@ var package_default = {
|
|
|
3101
3101
|
"gray-matter": "catalog:",
|
|
3102
3102
|
"isomorphic-git": "catalog:",
|
|
3103
3103
|
"js-sha1": "catalog:",
|
|
3104
|
-
"js-yaml": "^3.15.
|
|
3104
|
+
"js-yaml": "^3.15.2",
|
|
3105
3105
|
"jsonpath-plus": "catalog:",
|
|
3106
3106
|
"many-level": "catalog:",
|
|
3107
3107
|
micromatch: "catalog:",
|
|
@@ -3348,13 +3348,136 @@ import {
|
|
|
3348
3348
|
} from "graphql";
|
|
3349
3349
|
|
|
3350
3350
|
// src/resolver/index.ts
|
|
3351
|
-
import
|
|
3351
|
+
import path4 from "path";
|
|
3352
3352
|
import { isValid } from "date-fns";
|
|
3353
3353
|
import { JSONPath as JSONPath2 } from "jsonpath-plus";
|
|
3354
3354
|
|
|
3355
3355
|
// src/mdx/index.ts
|
|
3356
3356
|
import { parseMDX, serializeMDX } from "@tinacms/mdx";
|
|
3357
3357
|
|
|
3358
|
+
// src/references/visit.ts
|
|
3359
|
+
var MDX_SEGMENT = { mdx: true };
|
|
3360
|
+
var encodeLocator = (locator) => JSON.stringify(locator);
|
|
3361
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
3362
|
+
var visitReferences = (data, collectable, tinaSchema, visit) => {
|
|
3363
|
+
const visitCollectable = (collectableHere, value, locator) => {
|
|
3364
|
+
if (!isRecord(value)) {
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3367
|
+
let fields;
|
|
3368
|
+
try {
|
|
3369
|
+
fields = tinaSchema.getTemplateForData({
|
|
3370
|
+
data: value,
|
|
3371
|
+
collection: collectableHere
|
|
3372
|
+
})?.fields;
|
|
3373
|
+
} catch {
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
let replaced = false;
|
|
3377
|
+
for (const field of fields ?? []) {
|
|
3378
|
+
replaced = visitField(field, value, locator) || replaced;
|
|
3379
|
+
}
|
|
3380
|
+
return replaced;
|
|
3381
|
+
};
|
|
3382
|
+
const visitField = (field, parent, locator) => {
|
|
3383
|
+
const key = field.type === "rich-text" && field.isBody && "$_body" in parent ? "$_body" : field.name;
|
|
3384
|
+
const value = parent[key];
|
|
3385
|
+
if (value === void 0 || value === null) {
|
|
3386
|
+
return false;
|
|
3387
|
+
}
|
|
3388
|
+
const at = [...locator, field.name];
|
|
3389
|
+
let replaced = false;
|
|
3390
|
+
if (field.type === "reference") {
|
|
3391
|
+
if (field.list && Array.isArray(value)) {
|
|
3392
|
+
for (const [index, item] of value.entries()) {
|
|
3393
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
visit({
|
|
3397
|
+
value: item,
|
|
3398
|
+
collections: field.collections ?? [],
|
|
3399
|
+
locator: [...at, index],
|
|
3400
|
+
replace: (next) => {
|
|
3401
|
+
value[index] = next;
|
|
3402
|
+
replaced = true;
|
|
3403
|
+
}
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
return replaced;
|
|
3407
|
+
} else if (typeof value !== "string" || value.length === 0) {
|
|
3408
|
+
return false;
|
|
3409
|
+
}
|
|
3410
|
+
visit({
|
|
3411
|
+
value,
|
|
3412
|
+
collections: field.collections ?? [],
|
|
3413
|
+
locator: at,
|
|
3414
|
+
replace: (next) => {
|
|
3415
|
+
parent[field.name] = next;
|
|
3416
|
+
replaced = true;
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
return replaced;
|
|
3420
|
+
} else if (field.type === "object") {
|
|
3421
|
+
if (field.list && Array.isArray(value)) {
|
|
3422
|
+
for (const [index, item] of value.entries()) {
|
|
3423
|
+
replaced = visitCollectable(field, item, [...at, index]) || replaced;
|
|
3424
|
+
}
|
|
3425
|
+
return replaced;
|
|
3426
|
+
}
|
|
3427
|
+
return visitCollectable(field, value, at);
|
|
3428
|
+
} else if (field.type === "rich-text") {
|
|
3429
|
+
return visitRichText(field, parent, key, at);
|
|
3430
|
+
}
|
|
3431
|
+
return false;
|
|
3432
|
+
};
|
|
3433
|
+
const visitRichText = (field, parent, key, at) => {
|
|
3434
|
+
const stored = parent[key];
|
|
3435
|
+
const storedIsText = typeof stored === "string";
|
|
3436
|
+
if (!storedIsText && !isRecord(stored)) {
|
|
3437
|
+
return false;
|
|
3438
|
+
}
|
|
3439
|
+
let tree;
|
|
3440
|
+
if (storedIsText) {
|
|
3441
|
+
try {
|
|
3442
|
+
tree = parseMDX(stored, field, (value) => value);
|
|
3443
|
+
} catch {
|
|
3444
|
+
return false;
|
|
3445
|
+
}
|
|
3446
|
+
} else {
|
|
3447
|
+
tree = stored;
|
|
3448
|
+
}
|
|
3449
|
+
const replaced = visitNodes2(tree?.children, field, [...at, MDX_SEGMENT]);
|
|
3450
|
+
if (replaced && storedIsText) {
|
|
3451
|
+
parent[key] = serializeMDX(tree, field, (value) => value);
|
|
3452
|
+
}
|
|
3453
|
+
return replaced;
|
|
3454
|
+
};
|
|
3455
|
+
const visitNodes2 = (nodes, field, at) => {
|
|
3456
|
+
if (!Array.isArray(nodes)) {
|
|
3457
|
+
return false;
|
|
3458
|
+
}
|
|
3459
|
+
const templates = field.templates ?? [];
|
|
3460
|
+
let replaced = false;
|
|
3461
|
+
for (const [index, node] of nodes.entries()) {
|
|
3462
|
+
if (!isRecord(node)) {
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
const here = [...at, index];
|
|
3466
|
+
if (typeof node.name === "string" && (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && isRecord(node.props)) {
|
|
3467
|
+
const template = templates.find(
|
|
3468
|
+
(candidate) => lastItem(candidate.namespace) === node.name
|
|
3469
|
+
);
|
|
3470
|
+
for (const templateField of template?.fields ?? []) {
|
|
3471
|
+
replaced = visitField(templateField, node.props, [...here, "props"]) || replaced;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
replaced = visitNodes2(node.children, field, [...here, "children"]) || replaced;
|
|
3475
|
+
}
|
|
3476
|
+
return replaced;
|
|
3477
|
+
};
|
|
3478
|
+
return visitCollectable(collectable, data, []);
|
|
3479
|
+
};
|
|
3480
|
+
|
|
3358
3481
|
// src/resolver/index.ts
|
|
3359
3482
|
import {
|
|
3360
3483
|
ERR_ALREADY_EXISTS,
|
|
@@ -4431,54 +4554,41 @@ var makeIndexOpsForDocument = (filepath, collection, indexDefinitions, data, opT
|
|
|
4431
4554
|
}
|
|
4432
4555
|
return result;
|
|
4433
4556
|
};
|
|
4434
|
-
var makeRefOpsForDocument = (filepath,
|
|
4557
|
+
var makeRefOpsForDocument = (filepath, collectionName, tinaSchema, data, opType, level) => {
|
|
4435
4558
|
const result = [];
|
|
4436
|
-
if (
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
}
|
|
4457
|
-
if (references2[r]) {
|
|
4458
|
-
references2[r].push(path9);
|
|
4459
|
-
} else {
|
|
4460
|
-
references2[r] = [path9];
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4463
|
-
} else {
|
|
4464
|
-
if (references2[ref]) {
|
|
4465
|
-
references2[ref].push(path9);
|
|
4466
|
-
} else {
|
|
4467
|
-
references2[ref] = [path9];
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
}
|
|
4471
|
-
for (const ref of Object.keys(references2)) {
|
|
4472
|
-
for (const path9 of references2[ref]) {
|
|
4473
|
-
result.push({
|
|
4474
|
-
type: opType,
|
|
4475
|
-
key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${path9}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
|
|
4476
|
-
sublevel: refSublevel,
|
|
4477
|
-
value: opType === "put" ? {} : void 0
|
|
4478
|
-
});
|
|
4479
|
-
}
|
|
4559
|
+
if (!collectionName || !tinaSchema) {
|
|
4560
|
+
return result;
|
|
4561
|
+
}
|
|
4562
|
+
let collection;
|
|
4563
|
+
try {
|
|
4564
|
+
collection = tinaSchema.getCollection(collectionName);
|
|
4565
|
+
} catch {
|
|
4566
|
+
return result;
|
|
4567
|
+
}
|
|
4568
|
+
const byCollection = /* @__PURE__ */ new Map();
|
|
4569
|
+
visitReferences(
|
|
4570
|
+
data,
|
|
4571
|
+
collection,
|
|
4572
|
+
tinaSchema,
|
|
4573
|
+
({ value, collections, locator }) => {
|
|
4574
|
+
const encoded = encodeLocator(locator);
|
|
4575
|
+
for (const target of collections) {
|
|
4576
|
+
const entries = byCollection.get(target) ?? [];
|
|
4577
|
+
entries.push({ ref: value, locator: encoded });
|
|
4578
|
+
byCollection.set(target, entries);
|
|
4480
4579
|
}
|
|
4481
4580
|
}
|
|
4581
|
+
);
|
|
4582
|
+
for (const [target, entries] of byCollection) {
|
|
4583
|
+
const refSublevel = level.sublevel(target, SUBLEVEL_OPTIONS).sublevel(REFS_COLLECTIONS_SORT_KEY, SUBLEVEL_OPTIONS);
|
|
4584
|
+
for (const { ref, locator } of entries) {
|
|
4585
|
+
result.push({
|
|
4586
|
+
type: opType,
|
|
4587
|
+
key: `${ref}${INDEX_KEY_FIELD_SEPARATOR}${locator}${INDEX_KEY_FIELD_SEPARATOR}${filepath}`,
|
|
4588
|
+
sublevel: refSublevel,
|
|
4589
|
+
value: opType === "put" ? {} : void 0
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4482
4592
|
}
|
|
4483
4593
|
return result;
|
|
4484
4594
|
};
|
|
@@ -4683,6 +4793,170 @@ var collectConditionsForField = (fieldName, field, filterNode, pathExpression, c
|
|
|
4683
4793
|
}
|
|
4684
4794
|
};
|
|
4685
4795
|
|
|
4796
|
+
// src/resolver/auth-fields.ts
|
|
4797
|
+
import { randomBytes } from "crypto";
|
|
4798
|
+
import path3 from "path";
|
|
4799
|
+
async function getUserDocumentContext(tinaSchema, resolver) {
|
|
4800
|
+
const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
4801
|
+
if (!collection) {
|
|
4802
|
+
throw new Error("Auth collection not found");
|
|
4803
|
+
}
|
|
4804
|
+
const userFields = mapUserFields(collection, ["_rawData"]);
|
|
4805
|
+
if (!userFields.length) {
|
|
4806
|
+
throw new Error(`No user field found in collection ${collection.name}`);
|
|
4807
|
+
}
|
|
4808
|
+
if (userFields.length > 1) {
|
|
4809
|
+
throw new Error(
|
|
4810
|
+
`Multiple user fields found in collection ${collection.name}`
|
|
4811
|
+
);
|
|
4812
|
+
}
|
|
4813
|
+
const userField = userFields[0];
|
|
4814
|
+
const relativePath = "index.json";
|
|
4815
|
+
const realPath = path3.join(collection.path, relativePath);
|
|
4816
|
+
const userDoc = await resolver.getDocument(realPath);
|
|
4817
|
+
const users = get(userDoc, userField.path);
|
|
4818
|
+
if (!users) {
|
|
4819
|
+
throw new Error("No users found");
|
|
4820
|
+
}
|
|
4821
|
+
return { collection, userField, users, userDoc, relativePath };
|
|
4822
|
+
}
|
|
4823
|
+
function findUserInCollection(users, userField, userSub) {
|
|
4824
|
+
const { idFieldName } = userField;
|
|
4825
|
+
if (!idFieldName) {
|
|
4826
|
+
throw new Error("No uid field found on user field");
|
|
4827
|
+
}
|
|
4828
|
+
return users.find((u) => u[idFieldName] === userSub) || null;
|
|
4829
|
+
}
|
|
4830
|
+
function withoutPasswordValue(user, userField) {
|
|
4831
|
+
const { passwordFieldName } = userField;
|
|
4832
|
+
if (!passwordFieldName) {
|
|
4833
|
+
return user;
|
|
4834
|
+
}
|
|
4835
|
+
return {
|
|
4836
|
+
...user,
|
|
4837
|
+
[passwordFieldName]: {
|
|
4838
|
+
passwordChangeRequired: user[passwordFieldName]?.passwordChangeRequired ?? false
|
|
4839
|
+
}
|
|
4840
|
+
};
|
|
4841
|
+
}
|
|
4842
|
+
var dummyPasswordHash;
|
|
4843
|
+
var warnedUnflaggedAuthenticate = false;
|
|
4844
|
+
var getDummyPasswordHash = () => {
|
|
4845
|
+
dummyPasswordHash ??= generatePasswordHash({
|
|
4846
|
+
password: randomBytes(32).toString("hex")
|
|
4847
|
+
});
|
|
4848
|
+
return dummyPasswordHash;
|
|
4849
|
+
};
|
|
4850
|
+
function assertNewUsersHavePasswords(collection, newBody, existingData) {
|
|
4851
|
+
const userFields = mapUserFields(collection);
|
|
4852
|
+
if (userFields.length !== 1) {
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
const [{ path: usersPath, idFieldName, passwordFieldName }] = userFields;
|
|
4856
|
+
const users = get(newBody, usersPath);
|
|
4857
|
+
if (!Array.isArray(users) || !idFieldName || !passwordFieldName) {
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
const storedUsers = get(existingData, usersPath);
|
|
4861
|
+
const storedIds = new Set(
|
|
4862
|
+
(Array.isArray(storedUsers) ? storedUsers : []).map((u) => u?.[idFieldName])
|
|
4863
|
+
);
|
|
4864
|
+
const withoutPassword = users.filter(
|
|
4865
|
+
(u) => !storedIds.has(u?.[idFieldName]) && !u?.[passwordFieldName]?.["value"]
|
|
4866
|
+
).map((u) => u?.[idFieldName]);
|
|
4867
|
+
if (withoutPassword.length) {
|
|
4868
|
+
throw new Error(`New users need a password: ${withoutPassword.join(", ")}`);
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
async function handleAuthenticate({
|
|
4872
|
+
tinaSchema,
|
|
4873
|
+
resolver,
|
|
4874
|
+
sub,
|
|
4875
|
+
password,
|
|
4876
|
+
isSignIn
|
|
4877
|
+
}) {
|
|
4878
|
+
if (!isSignIn) {
|
|
4879
|
+
if (!warnedUnflaggedAuthenticate) {
|
|
4880
|
+
warnedUnflaggedAuthenticate = true;
|
|
4881
|
+
console.warn(
|
|
4882
|
+
"TinaCMS: authenticate was called without isSignIn, so it returned null. If sign-in is failing, run `tinacms build` with a @tinacms/cli that matches @tinacms/datalayer to regenerate the database client."
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
4885
|
+
return null;
|
|
4886
|
+
}
|
|
4887
|
+
const { userField, users } = await getUserDocumentContext(
|
|
4888
|
+
tinaSchema,
|
|
4889
|
+
resolver
|
|
4890
|
+
);
|
|
4891
|
+
const user = sub ? findUserInCollection(users, userField, sub) : null;
|
|
4892
|
+
const { passwordFieldName } = userField;
|
|
4893
|
+
const saltedHash = user ? get(user, [passwordFieldName || "", "value"]) : void 0;
|
|
4894
|
+
if (!saltedHash) {
|
|
4895
|
+
await checkPasswordHash({
|
|
4896
|
+
saltedHash: await getDummyPasswordHash(),
|
|
4897
|
+
password: password ?? ""
|
|
4898
|
+
});
|
|
4899
|
+
return null;
|
|
4900
|
+
}
|
|
4901
|
+
const matches = await checkPasswordHash({
|
|
4902
|
+
saltedHash,
|
|
4903
|
+
password
|
|
4904
|
+
});
|
|
4905
|
+
return matches ? withoutPasswordValue(user, userField) : null;
|
|
4906
|
+
}
|
|
4907
|
+
async function handleAuthorize({
|
|
4908
|
+
tinaSchema,
|
|
4909
|
+
resolver,
|
|
4910
|
+
sub,
|
|
4911
|
+
ctxUser
|
|
4912
|
+
}) {
|
|
4913
|
+
const userSub = sub || ctxUser?.sub;
|
|
4914
|
+
const { userField, users } = await getUserDocumentContext(
|
|
4915
|
+
tinaSchema,
|
|
4916
|
+
resolver
|
|
4917
|
+
);
|
|
4918
|
+
const user = findUserInCollection(users, userField, userSub);
|
|
4919
|
+
return user ? withoutPasswordValue(user, userField) : null;
|
|
4920
|
+
}
|
|
4921
|
+
async function handleUpdatePassword({
|
|
4922
|
+
tinaSchema,
|
|
4923
|
+
resolver,
|
|
4924
|
+
password,
|
|
4925
|
+
ctxUser
|
|
4926
|
+
}) {
|
|
4927
|
+
if (!ctxUser?.sub) {
|
|
4928
|
+
throw new Error("Not authorized");
|
|
4929
|
+
}
|
|
4930
|
+
if (!password) {
|
|
4931
|
+
throw new Error("No password provided");
|
|
4932
|
+
}
|
|
4933
|
+
const { userField, users } = await getUserDocumentContext(
|
|
4934
|
+
tinaSchema,
|
|
4935
|
+
resolver
|
|
4936
|
+
);
|
|
4937
|
+
const { idFieldName, passwordFieldName } = userField;
|
|
4938
|
+
if (!passwordFieldName) {
|
|
4939
|
+
throw new Error("No password field found on user field");
|
|
4940
|
+
}
|
|
4941
|
+
if (!users.find((u) => u[idFieldName] === ctxUser.sub)) {
|
|
4942
|
+
throw new Error("Not authorized");
|
|
4943
|
+
}
|
|
4944
|
+
const passwordHash = await generatePasswordHash({ password });
|
|
4945
|
+
await resolver.updateAuthDocumentInternal((rawData) => {
|
|
4946
|
+
const storedUsers = get(rawData, userField.path.slice(1));
|
|
4947
|
+
const user = Array.isArray(storedUsers) ? storedUsers.find((u) => u[idFieldName] === ctxUser.sub) : void 0;
|
|
4948
|
+
if (!user) {
|
|
4949
|
+
throw new Error("Not authorized");
|
|
4950
|
+
}
|
|
4951
|
+
user[passwordFieldName] = {
|
|
4952
|
+
value: passwordHash,
|
|
4953
|
+
passwordChangeRequired: false
|
|
4954
|
+
};
|
|
4955
|
+
return rawData;
|
|
4956
|
+
});
|
|
4957
|
+
return true;
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4686
4960
|
// src/resolver/media-utils.ts
|
|
4687
4961
|
var resolveMediaCloudToRelative = (value, config = { useRelativeMedia: true }, schema) => {
|
|
4688
4962
|
if (config && value) {
|
|
@@ -4764,6 +5038,7 @@ var hasTinaMediaConfig = (schema) => {
|
|
|
4764
5038
|
};
|
|
4765
5039
|
|
|
4766
5040
|
// src/resolver/index.ts
|
|
5041
|
+
var authCollectionWrites = /* @__PURE__ */ new WeakMap();
|
|
4767
5042
|
var createResolver = (args) => {
|
|
4768
5043
|
return new Resolver(args);
|
|
4769
5044
|
};
|
|
@@ -4903,7 +5178,7 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4903
5178
|
base: basename,
|
|
4904
5179
|
ext: extension,
|
|
4905
5180
|
name: filename
|
|
4906
|
-
} =
|
|
5181
|
+
} = path4.parse(normalizedPath);
|
|
4907
5182
|
const relativePath = normalizedPath.replace(collection.path, "").replace(/^\/|\/$/g, "");
|
|
4908
5183
|
const breadcrumbs = relativePath.replace(extension, "").split("/");
|
|
4909
5184
|
const data = {
|
|
@@ -4965,34 +5240,6 @@ var transformDocumentIntoPayload = async (fullPath, rawData, tinaSchema, config,
|
|
|
4965
5240
|
throw e;
|
|
4966
5241
|
}
|
|
4967
5242
|
};
|
|
4968
|
-
var updateObjectWithJsonPath = (obj, path9, oldValue, newValue) => {
|
|
4969
|
-
let updated = false;
|
|
4970
|
-
if (!path9.includes(".") && !path9.includes("[")) {
|
|
4971
|
-
if (path9 in obj && obj[path9] === oldValue) {
|
|
4972
|
-
obj[path9] = newValue;
|
|
4973
|
-
updated = true;
|
|
4974
|
-
}
|
|
4975
|
-
return { object: obj, updated };
|
|
4976
|
-
}
|
|
4977
|
-
const parentPath = path9.replace(/\.[^.\[\]]+$/, "");
|
|
4978
|
-
const keyToUpdate = path9.match(/[^.\[\]]+$/)[0];
|
|
4979
|
-
const parents = JSONPath2({
|
|
4980
|
-
path: parentPath,
|
|
4981
|
-
json: obj,
|
|
4982
|
-
resultType: "value"
|
|
4983
|
-
});
|
|
4984
|
-
if (parents.length > 0) {
|
|
4985
|
-
parents.forEach((parent) => {
|
|
4986
|
-
if (parent && typeof parent === "object" && keyToUpdate in parent) {
|
|
4987
|
-
if (parent[keyToUpdate] === oldValue) {
|
|
4988
|
-
parent[keyToUpdate] = newValue;
|
|
4989
|
-
updated = true;
|
|
4990
|
-
}
|
|
4991
|
-
}
|
|
4992
|
-
});
|
|
4993
|
-
}
|
|
4994
|
-
return { object: obj, updated };
|
|
4995
|
-
};
|
|
4996
5243
|
var Resolver = class _Resolver {
|
|
4997
5244
|
constructor(init) {
|
|
4998
5245
|
this.init = init;
|
|
@@ -5000,12 +5247,14 @@ var Resolver = class _Resolver {
|
|
|
5000
5247
|
this.database = init.database;
|
|
5001
5248
|
this.tinaSchema = init.tinaSchema;
|
|
5002
5249
|
this.isAudit = init.isAudit;
|
|
5250
|
+
this.ctxUser = init.ctxUser;
|
|
5003
5251
|
}
|
|
5004
5252
|
init;
|
|
5005
5253
|
config;
|
|
5006
5254
|
database;
|
|
5007
5255
|
tinaSchema;
|
|
5008
5256
|
isAudit;
|
|
5257
|
+
ctxUser;
|
|
5009
5258
|
resolveCollection = async (_args, collectionName, hasDocuments) => {
|
|
5010
5259
|
const collection = this.tinaSchema.getCollection(collectionName);
|
|
5011
5260
|
const extraFields = {};
|
|
@@ -5182,6 +5431,11 @@ var Resolver = class _Resolver {
|
|
|
5182
5431
|
collectionName,
|
|
5183
5432
|
relativePath
|
|
5184
5433
|
);
|
|
5434
|
+
await this.assertAuthCollectionWrite({
|
|
5435
|
+
collection,
|
|
5436
|
+
realPath,
|
|
5437
|
+
op: "addPending"
|
|
5438
|
+
});
|
|
5185
5439
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5186
5440
|
if (alreadyExists) {
|
|
5187
5441
|
throw new Error(
|
|
@@ -5297,19 +5551,19 @@ var Resolver = class _Resolver {
|
|
|
5297
5551
|
if (fullPath.includes("\0")) {
|
|
5298
5552
|
throw new Error("Invalid path: null bytes are not allowed");
|
|
5299
5553
|
}
|
|
5300
|
-
const normalizedPath =
|
|
5301
|
-
const normalizedCollectionPath =
|
|
5302
|
-
const relative =
|
|
5554
|
+
const normalizedPath = path4.normalize(fullPath.replace(/\\/g, "/"));
|
|
5555
|
+
const normalizedCollectionPath = path4.normalize(collection.path);
|
|
5556
|
+
const relative = path4.relative(normalizedCollectionPath, normalizedPath);
|
|
5303
5557
|
if (relative.startsWith("..")) {
|
|
5304
5558
|
throw new Error(`Invalid path: path escapes the collection directory`);
|
|
5305
5559
|
}
|
|
5306
|
-
if (
|
|
5560
|
+
if (path4.isAbsolute(relative)) {
|
|
5307
5561
|
throw new Error(`Invalid path: absolute paths are not allowed`);
|
|
5308
5562
|
}
|
|
5309
5563
|
if (relativePath) {
|
|
5310
5564
|
_Resolver.validateRelativePath(relativePath);
|
|
5311
5565
|
const collectionFormat = collection.format || "md";
|
|
5312
|
-
const fileExtension =
|
|
5566
|
+
const fileExtension = path4.extname(relativePath).toLowerCase().slice(1);
|
|
5313
5567
|
if (fileExtension !== collectionFormat) {
|
|
5314
5568
|
throw new Error(
|
|
5315
5569
|
`Invalid file extension: expected '.${collectionFormat}' but got '.${fileExtension}'`
|
|
@@ -5317,6 +5571,81 @@ var Resolver = class _Resolver {
|
|
|
5317
5571
|
}
|
|
5318
5572
|
}
|
|
5319
5573
|
};
|
|
5574
|
+
assertAuthCollectionWrite = async ({
|
|
5575
|
+
collection,
|
|
5576
|
+
realPath,
|
|
5577
|
+
op
|
|
5578
|
+
}) => {
|
|
5579
|
+
const authCollection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
5580
|
+
if (!authCollection) {
|
|
5581
|
+
return;
|
|
5582
|
+
}
|
|
5583
|
+
const normalizedPath = normalizePath2(realPath);
|
|
5584
|
+
if (collection.name !== authCollection.name && !this.isStoredInCollection(normalizedPath, authCollection)) {
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
const { admins, allowUnauthenticatedWrites } = this.database.authCollection ?? { admins: [], allowUnauthenticatedWrites: true };
|
|
5588
|
+
if (this.ctxUser === void 0 && allowUnauthenticatedWrites) {
|
|
5589
|
+
return;
|
|
5590
|
+
}
|
|
5591
|
+
const sub = this.ctxUser?.sub;
|
|
5592
|
+
const userStorePath = normalizePath2(
|
|
5593
|
+
path4.join(authCollection.path, "index.json")
|
|
5594
|
+
);
|
|
5595
|
+
if (op === "update" && normalizedPath === userStorePath && typeof sub === "string" && sub !== "" && admins.includes(sub) && await this.isStoredUser(sub)) {
|
|
5596
|
+
return;
|
|
5597
|
+
}
|
|
5598
|
+
throw new Error("Not authorized");
|
|
5599
|
+
};
|
|
5600
|
+
assertReferenceUpdates = async (realPath, collection) => {
|
|
5601
|
+
if (!this.tinaSchema.getCollections().some((c) => c.isAuthCollection)) {
|
|
5602
|
+
return;
|
|
5603
|
+
}
|
|
5604
|
+
const collRefs = await this.findReferences(realPath, collection);
|
|
5605
|
+
for (const docsWithRefs of Object.values(collRefs)) {
|
|
5606
|
+
for (const pathToDocWithRef of Object.keys(docsWithRefs)) {
|
|
5607
|
+
await this.assertAuthCollectionWrite({
|
|
5608
|
+
collection: this.tinaSchema.getCollectionByFullPath(pathToDocWithRef),
|
|
5609
|
+
realPath: pathToDocWithRef,
|
|
5610
|
+
op: "update"
|
|
5611
|
+
});
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
};
|
|
5615
|
+
isAuthCollectionPath = (realPath) => {
|
|
5616
|
+
const authCollection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
5617
|
+
return !!authCollection && this.isStoredInCollection(normalizePath2(realPath), authCollection);
|
|
5618
|
+
};
|
|
5619
|
+
withAuthCollectionLock = (realPath, write) => {
|
|
5620
|
+
if (!this.isAuthCollectionPath(realPath)) {
|
|
5621
|
+
return write();
|
|
5622
|
+
}
|
|
5623
|
+
const previous = authCollectionWrites.get(this.database);
|
|
5624
|
+
const current = previous ? previous.then(write, write) : write();
|
|
5625
|
+
authCollectionWrites.set(
|
|
5626
|
+
this.database,
|
|
5627
|
+
current.catch(() => void 0)
|
|
5628
|
+
);
|
|
5629
|
+
return current;
|
|
5630
|
+
};
|
|
5631
|
+
isStoredInCollection = (realPath, collection) => {
|
|
5632
|
+
try {
|
|
5633
|
+
return this.tinaSchema.getCollectionByFullPath(realPath)?.name === collection.name;
|
|
5634
|
+
} catch {
|
|
5635
|
+
return false;
|
|
5636
|
+
}
|
|
5637
|
+
};
|
|
5638
|
+
isStoredUser = async (sub) => {
|
|
5639
|
+
try {
|
|
5640
|
+
const { userField, users } = await getUserDocumentContext(
|
|
5641
|
+
this.tinaSchema,
|
|
5642
|
+
this
|
|
5643
|
+
);
|
|
5644
|
+
return Array.isArray(users) && !!findUserInCollection(users, userField, sub);
|
|
5645
|
+
} catch {
|
|
5646
|
+
return false;
|
|
5647
|
+
}
|
|
5648
|
+
};
|
|
5320
5649
|
/**
|
|
5321
5650
|
* Helper method to get collection and construct validated path.
|
|
5322
5651
|
* This encapsulates the common pattern of getting a collection, joining paths,
|
|
@@ -5337,7 +5666,7 @@ var Resolver = class _Resolver {
|
|
|
5337
5666
|
if (options?.extraSegments) {
|
|
5338
5667
|
pathSegments.push(...options.extraSegments.map(_Resolver.sanitizePath));
|
|
5339
5668
|
}
|
|
5340
|
-
const realPath = normalizePath2(
|
|
5669
|
+
const realPath = normalizePath2(path4.join(...pathSegments));
|
|
5341
5670
|
const shouldValidateExtension = options?.validateExtension !== false;
|
|
5342
5671
|
this.validatePath(
|
|
5343
5672
|
realPath,
|
|
@@ -5371,12 +5700,17 @@ var Resolver = class _Resolver {
|
|
|
5371
5700
|
}) => {
|
|
5372
5701
|
const collection = this.getCollectionWithName(collectionName);
|
|
5373
5702
|
_Resolver.validateRelativePath(relativePath);
|
|
5374
|
-
const realPath =
|
|
5703
|
+
const realPath = path4.join(
|
|
5375
5704
|
collection.path,
|
|
5376
5705
|
relativePath,
|
|
5377
5706
|
`.gitkeep.${collection.format || "md"}`
|
|
5378
5707
|
);
|
|
5379
5708
|
this.validatePath(realPath, collection);
|
|
5709
|
+
await this.assertAuthCollectionWrite({
|
|
5710
|
+
collection,
|
|
5711
|
+
realPath,
|
|
5712
|
+
op: "createFolder"
|
|
5713
|
+
});
|
|
5380
5714
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5381
5715
|
if (alreadyExists) {
|
|
5382
5716
|
throw new Error(
|
|
@@ -5399,6 +5733,11 @@ var Resolver = class _Resolver {
|
|
|
5399
5733
|
collectionName,
|
|
5400
5734
|
relativePath
|
|
5401
5735
|
);
|
|
5736
|
+
await this.assertAuthCollectionWrite({
|
|
5737
|
+
collection,
|
|
5738
|
+
realPath,
|
|
5739
|
+
op: "create"
|
|
5740
|
+
});
|
|
5402
5741
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5403
5742
|
if (alreadyExists) {
|
|
5404
5743
|
throw new Error(
|
|
@@ -5409,7 +5748,17 @@ var Resolver = class _Resolver {
|
|
|
5409
5748
|
await this.database.put(realPath, params, collection.name);
|
|
5410
5749
|
return this.getDocument(realPath);
|
|
5411
5750
|
};
|
|
5412
|
-
resolveUpdateDocument = async ({
|
|
5751
|
+
resolveUpdateDocument = async (args) => {
|
|
5752
|
+
const { realPath } = this.getValidatedPath(
|
|
5753
|
+
args.collectionName,
|
|
5754
|
+
args.relativePath
|
|
5755
|
+
);
|
|
5756
|
+
return this.withAuthCollectionLock(
|
|
5757
|
+
realPath,
|
|
5758
|
+
() => this.updateDocument(args)
|
|
5759
|
+
);
|
|
5760
|
+
};
|
|
5761
|
+
updateDocument = async ({
|
|
5413
5762
|
collectionName,
|
|
5414
5763
|
relativePath,
|
|
5415
5764
|
newRelativePath,
|
|
@@ -5419,6 +5768,13 @@ var Resolver = class _Resolver {
|
|
|
5419
5768
|
collectionName,
|
|
5420
5769
|
relativePath
|
|
5421
5770
|
);
|
|
5771
|
+
if (!newRelativePath) {
|
|
5772
|
+
await this.assertAuthCollectionWrite({
|
|
5773
|
+
collection,
|
|
5774
|
+
realPath,
|
|
5775
|
+
op: "update"
|
|
5776
|
+
});
|
|
5777
|
+
}
|
|
5422
5778
|
const alreadyExists = await this.database.documentExists(realPath);
|
|
5423
5779
|
if (!alreadyExists) {
|
|
5424
5780
|
throw new Error(`Unable to update document, ${realPath} does not exist`);
|
|
@@ -5438,45 +5794,33 @@ var Resolver = class _Resolver {
|
|
|
5438
5794
|
`Unable to rename document, ${newRealPath} ${ERR_ALREADY_EXISTS}`
|
|
5439
5795
|
);
|
|
5440
5796
|
}
|
|
5797
|
+
await this.assertAuthCollectionWrite({
|
|
5798
|
+
collection,
|
|
5799
|
+
realPath,
|
|
5800
|
+
op: "rename"
|
|
5801
|
+
});
|
|
5802
|
+
await this.assertAuthCollectionWrite({
|
|
5803
|
+
collection,
|
|
5804
|
+
realPath: newRealPath,
|
|
5805
|
+
op: "rename"
|
|
5806
|
+
});
|
|
5807
|
+
await this.assertReferenceUpdates(realPath, collection);
|
|
5441
5808
|
await this.database.put(newRealPath, doc._rawData, collection.name);
|
|
5442
5809
|
await this.deleteDocument(realPath);
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
)
|
|
5448
|
-
|
|
5449
|
-
let hasUpdate = false;
|
|
5450
|
-
for (const path9 of referencePaths) {
|
|
5451
|
-
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5452
|
-
docWithRef,
|
|
5453
|
-
path9,
|
|
5454
|
-
realPath,
|
|
5455
|
-
newRealPath
|
|
5456
|
-
);
|
|
5457
|
-
docWithRef = object2;
|
|
5458
|
-
hasUpdate = updated || hasUpdate;
|
|
5459
|
-
}
|
|
5460
|
-
if (hasUpdate) {
|
|
5461
|
-
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5462
|
-
if (!collectionWithRef) {
|
|
5463
|
-
throw new Error(
|
|
5464
|
-
`Unable to find collection for ${pathToDocWithRef}`
|
|
5465
|
-
);
|
|
5466
|
-
}
|
|
5467
|
-
await this.database.put(
|
|
5468
|
-
pathToDocWithRef,
|
|
5469
|
-
docWithRef,
|
|
5470
|
-
collectionWithRef.name
|
|
5471
|
-
);
|
|
5472
|
-
}
|
|
5473
|
-
}
|
|
5474
|
-
}
|
|
5810
|
+
await this.rewriteReferences(
|
|
5811
|
+
realPath,
|
|
5812
|
+
newRealPath,
|
|
5813
|
+
collection,
|
|
5814
|
+
this.isAuthCollectionPath(realPath)
|
|
5815
|
+
);
|
|
5475
5816
|
return this.getDocument(newRealPath);
|
|
5476
5817
|
}
|
|
5477
5818
|
if (!newBody) {
|
|
5478
5819
|
throw new Error("Body not provided for updated document.");
|
|
5479
5820
|
}
|
|
5821
|
+
if (collection.isAuthCollection) {
|
|
5822
|
+
assertNewUsersHavePasswords(collection, newBody, doc?._rawData);
|
|
5823
|
+
}
|
|
5480
5824
|
const params = await this.buildObjectMutations(
|
|
5481
5825
|
newBody,
|
|
5482
5826
|
collection,
|
|
@@ -5493,6 +5837,21 @@ var Resolver = class _Resolver {
|
|
|
5493
5837
|
);
|
|
5494
5838
|
return this.getDocument(realPath);
|
|
5495
5839
|
};
|
|
5840
|
+
/**
|
|
5841
|
+
* Rewrites the auth collection's user store without the admin check, so the
|
|
5842
|
+
* caller must check the request user.
|
|
5843
|
+
*/
|
|
5844
|
+
updateAuthDocumentInternal = async (update) => {
|
|
5845
|
+
const collection = this.tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
5846
|
+
if (!collection) {
|
|
5847
|
+
throw new Error("Auth collection not found");
|
|
5848
|
+
}
|
|
5849
|
+
const { realPath } = this.getValidatedPath(collection.name, "index.json");
|
|
5850
|
+
await this.withAuthCollectionLock(realPath, async () => {
|
|
5851
|
+
const rawData = await this.getRaw(realPath);
|
|
5852
|
+
await this.database.put(realPath, update(rawData), collection.name);
|
|
5853
|
+
});
|
|
5854
|
+
};
|
|
5496
5855
|
resolveDeleteDocument = async ({
|
|
5497
5856
|
collectionName,
|
|
5498
5857
|
relativePath
|
|
@@ -5505,41 +5864,19 @@ var Resolver = class _Resolver {
|
|
|
5505
5864
|
if (!alreadyExists) {
|
|
5506
5865
|
throw new Error(`Unable to delete document, ${realPath} does not exist`);
|
|
5507
5866
|
}
|
|
5867
|
+
await this.assertAuthCollectionWrite({
|
|
5868
|
+
collection,
|
|
5869
|
+
realPath,
|
|
5870
|
+
op: "delete"
|
|
5871
|
+
});
|
|
5872
|
+
const hasReferences = await this.hasReferences(realPath, collection);
|
|
5873
|
+
if (hasReferences) {
|
|
5874
|
+
await this.assertReferenceUpdates(realPath, collection);
|
|
5875
|
+
}
|
|
5508
5876
|
const doc = await this.getDocument(realPath);
|
|
5509
5877
|
await this.deleteDocument(realPath);
|
|
5510
|
-
if (
|
|
5511
|
-
|
|
5512
|
-
for (const [_collection, docsWithRefs] of Object.entries(collRefs)) {
|
|
5513
|
-
for (const [pathToDocWithRef, referencePaths] of Object.entries(
|
|
5514
|
-
docsWithRefs
|
|
5515
|
-
)) {
|
|
5516
|
-
let refDoc = await this.getRaw(pathToDocWithRef);
|
|
5517
|
-
let hasUpdate = false;
|
|
5518
|
-
for (const path9 of referencePaths) {
|
|
5519
|
-
const { object: object2, updated } = updateObjectWithJsonPath(
|
|
5520
|
-
refDoc,
|
|
5521
|
-
path9,
|
|
5522
|
-
realPath,
|
|
5523
|
-
null
|
|
5524
|
-
);
|
|
5525
|
-
refDoc = object2;
|
|
5526
|
-
hasUpdate = updated || hasUpdate;
|
|
5527
|
-
}
|
|
5528
|
-
if (hasUpdate) {
|
|
5529
|
-
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
5530
|
-
if (!collectionWithRef) {
|
|
5531
|
-
throw new Error(
|
|
5532
|
-
`Unable to find collection for ${pathToDocWithRef}`
|
|
5533
|
-
);
|
|
5534
|
-
}
|
|
5535
|
-
await this.database.put(
|
|
5536
|
-
pathToDocWithRef,
|
|
5537
|
-
refDoc,
|
|
5538
|
-
collectionWithRef.name
|
|
5539
|
-
);
|
|
5540
|
-
}
|
|
5541
|
-
}
|
|
5542
|
-
}
|
|
5878
|
+
if (hasReferences) {
|
|
5879
|
+
await this.rewriteReferences(realPath, null, collection, false);
|
|
5543
5880
|
}
|
|
5544
5881
|
return doc;
|
|
5545
5882
|
};
|
|
@@ -5573,10 +5910,15 @@ var Resolver = class _Resolver {
|
|
|
5573
5910
|
if (isAddPendingDocument === true) {
|
|
5574
5911
|
return this.resolveAddPendingDocument({
|
|
5575
5912
|
collectionName: collection.name,
|
|
5576
|
-
relativePath:
|
|
5913
|
+
relativePath: path4.relative(collection.path, realPath),
|
|
5577
5914
|
templateName: args.template
|
|
5578
5915
|
});
|
|
5579
5916
|
}
|
|
5917
|
+
await this.assertAuthCollectionWrite({
|
|
5918
|
+
collection,
|
|
5919
|
+
realPath,
|
|
5920
|
+
op: "create"
|
|
5921
|
+
});
|
|
5580
5922
|
const params = await this.buildObjectMutations(
|
|
5581
5923
|
// @ts-ignore
|
|
5582
5924
|
args.params[collection.name],
|
|
@@ -5595,6 +5937,11 @@ var Resolver = class _Resolver {
|
|
|
5595
5937
|
isAddPendingDocument,
|
|
5596
5938
|
isCollectionSpecific
|
|
5597
5939
|
}) => {
|
|
5940
|
+
await this.assertAuthCollectionWrite({
|
|
5941
|
+
collection,
|
|
5942
|
+
realPath,
|
|
5943
|
+
op: "update"
|
|
5944
|
+
});
|
|
5598
5945
|
const doc = await this.getDocument(realPath);
|
|
5599
5946
|
const oldDoc = this.resolveLegacyValues(doc?._rawData || {}, collection);
|
|
5600
5947
|
if (isAddPendingDocument === true) {
|
|
@@ -5887,6 +6234,47 @@ var Resolver = class _Resolver {
|
|
|
5887
6234
|
}
|
|
5888
6235
|
};
|
|
5889
6236
|
};
|
|
6237
|
+
/**
|
|
6238
|
+
* Repoints every reference to `from` at `to`, or clears it when `to` is null.
|
|
6239
|
+
*
|
|
6240
|
+
* Each referring document is re-walked rather than addressed by the locators the index stored,
|
|
6241
|
+
* since a stored path cannot address a reference inside rich text. Only a document the walk
|
|
6242
|
+
* changed is written back.
|
|
6243
|
+
*/
|
|
6244
|
+
rewriteReferences = async (from, to, collection, holdsLock) => {
|
|
6245
|
+
const collRefs = await this.findReferences(from, collection);
|
|
6246
|
+
for (const docsWithRefs of Object.values(collRefs)) {
|
|
6247
|
+
for (const pathToDocWithRef of Object.keys(docsWithRefs)) {
|
|
6248
|
+
const rewrite = async () => {
|
|
6249
|
+
const collectionWithRef = this.tinaSchema.getCollectionByFullPath(pathToDocWithRef);
|
|
6250
|
+
if (!collectionWithRef) {
|
|
6251
|
+
throw new Error(
|
|
6252
|
+
`Unable to find collection for ${pathToDocWithRef}`
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
const docWithRef = await this.getRaw(pathToDocWithRef);
|
|
6256
|
+
const changed = visitReferences(
|
|
6257
|
+
docWithRef,
|
|
6258
|
+
collectionWithRef,
|
|
6259
|
+
this.tinaSchema,
|
|
6260
|
+
(reference) => {
|
|
6261
|
+
if (reference.value === from) {
|
|
6262
|
+
reference.replace(to);
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
);
|
|
6266
|
+
if (changed) {
|
|
6267
|
+
await this.database.put(
|
|
6268
|
+
pathToDocWithRef,
|
|
6269
|
+
docWithRef,
|
|
6270
|
+
collectionWithRef.name
|
|
6271
|
+
);
|
|
6272
|
+
}
|
|
6273
|
+
};
|
|
6274
|
+
await (holdsLock ? rewrite() : this.withAuthCollectionLock(pathToDocWithRef, rewrite));
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
};
|
|
5890
6278
|
/**
|
|
5891
6279
|
* Checks if a document has references to it
|
|
5892
6280
|
* @param id The id of the document to check for references
|
|
@@ -5897,6 +6285,8 @@ var Resolver = class _Resolver {
|
|
|
5897
6285
|
let count = 0;
|
|
5898
6286
|
await this.database.query(
|
|
5899
6287
|
{
|
|
6288
|
+
// One key answers a boolean; the default page is 50.
|
|
6289
|
+
first: 1,
|
|
5900
6290
|
collection: c.name,
|
|
5901
6291
|
filterChain: makeFilterChain({
|
|
5902
6292
|
conditions: [
|
|
@@ -5932,6 +6322,9 @@ var Resolver = class _Resolver {
|
|
|
5932
6322
|
const references = {};
|
|
5933
6323
|
await this.database.query(
|
|
5934
6324
|
{
|
|
6325
|
+
// Every reference, not the default page of 50, which otherwise caps how many a rename or
|
|
6326
|
+
// delete updates.
|
|
6327
|
+
first: -1,
|
|
5935
6328
|
collection: c.name,
|
|
5936
6329
|
filterChain: makeFilterChain({
|
|
5937
6330
|
conditions: [
|
|
@@ -6101,130 +6494,6 @@ var resolveDateInput = (value) => {
|
|
|
6101
6494
|
return date;
|
|
6102
6495
|
};
|
|
6103
6496
|
|
|
6104
|
-
// src/resolver/auth-fields.ts
|
|
6105
|
-
import path4 from "path";
|
|
6106
|
-
import { set } from "es-toolkit/compat";
|
|
6107
|
-
async function getUserDocumentContext(tinaSchema, resolver) {
|
|
6108
|
-
const collection = tinaSchema.getCollections().find((c) => c.isAuthCollection);
|
|
6109
|
-
if (!collection) {
|
|
6110
|
-
throw new Error("Auth collection not found");
|
|
6111
|
-
}
|
|
6112
|
-
const userFields = mapUserFields(collection, ["_rawData"]);
|
|
6113
|
-
if (!userFields.length) {
|
|
6114
|
-
throw new Error(`No user field found in collection ${collection.name}`);
|
|
6115
|
-
}
|
|
6116
|
-
if (userFields.length > 1) {
|
|
6117
|
-
throw new Error(
|
|
6118
|
-
`Multiple user fields found in collection ${collection.name}`
|
|
6119
|
-
);
|
|
6120
|
-
}
|
|
6121
|
-
const userField = userFields[0];
|
|
6122
|
-
const relativePath = "index.json";
|
|
6123
|
-
const realPath = path4.join(collection.path, relativePath);
|
|
6124
|
-
const userDoc = await resolver.getDocument(realPath);
|
|
6125
|
-
const users = get(userDoc, userField.path);
|
|
6126
|
-
if (!users) {
|
|
6127
|
-
throw new Error("No users found");
|
|
6128
|
-
}
|
|
6129
|
-
return { collection, userField, users, userDoc, relativePath };
|
|
6130
|
-
}
|
|
6131
|
-
function findUserInCollection(users, userField, userSub) {
|
|
6132
|
-
const { idFieldName } = userField;
|
|
6133
|
-
if (!idFieldName) {
|
|
6134
|
-
throw new Error("No uid field found on user field");
|
|
6135
|
-
}
|
|
6136
|
-
return users.find((u) => u[idFieldName] === userSub) || null;
|
|
6137
|
-
}
|
|
6138
|
-
async function handleAuthenticate({
|
|
6139
|
-
tinaSchema,
|
|
6140
|
-
resolver,
|
|
6141
|
-
sub,
|
|
6142
|
-
password,
|
|
6143
|
-
ctxUser
|
|
6144
|
-
}) {
|
|
6145
|
-
const userSub = sub || ctxUser?.sub;
|
|
6146
|
-
const { userField, users } = await getUserDocumentContext(
|
|
6147
|
-
tinaSchema,
|
|
6148
|
-
resolver
|
|
6149
|
-
);
|
|
6150
|
-
const user = findUserInCollection(users, userField, userSub);
|
|
6151
|
-
if (!user) {
|
|
6152
|
-
return null;
|
|
6153
|
-
}
|
|
6154
|
-
const { passwordFieldName } = userField;
|
|
6155
|
-
const saltedHash = get(user, [passwordFieldName || "", "value"]);
|
|
6156
|
-
if (!saltedHash) {
|
|
6157
|
-
throw new Error("No password field found on user field");
|
|
6158
|
-
}
|
|
6159
|
-
const matches = await checkPasswordHash({
|
|
6160
|
-
saltedHash,
|
|
6161
|
-
password
|
|
6162
|
-
});
|
|
6163
|
-
return matches ? user : null;
|
|
6164
|
-
}
|
|
6165
|
-
async function handleAuthorize({
|
|
6166
|
-
tinaSchema,
|
|
6167
|
-
resolver,
|
|
6168
|
-
sub,
|
|
6169
|
-
ctxUser
|
|
6170
|
-
}) {
|
|
6171
|
-
const userSub = sub || ctxUser?.sub;
|
|
6172
|
-
const { userField, users } = await getUserDocumentContext(
|
|
6173
|
-
tinaSchema,
|
|
6174
|
-
resolver
|
|
6175
|
-
);
|
|
6176
|
-
const user = findUserInCollection(users, userField, userSub);
|
|
6177
|
-
return user ? user : null;
|
|
6178
|
-
}
|
|
6179
|
-
async function handleUpdatePassword({
|
|
6180
|
-
tinaSchema,
|
|
6181
|
-
resolver,
|
|
6182
|
-
password,
|
|
6183
|
-
ctxUser
|
|
6184
|
-
}) {
|
|
6185
|
-
if (!ctxUser?.sub) {
|
|
6186
|
-
throw new Error("Not authorized");
|
|
6187
|
-
}
|
|
6188
|
-
if (!password) {
|
|
6189
|
-
throw new Error("No password provided");
|
|
6190
|
-
}
|
|
6191
|
-
const { collection, userField, users, relativePath } = await getUserDocumentContext(tinaSchema, resolver);
|
|
6192
|
-
const { idFieldName, passwordFieldName } = userField;
|
|
6193
|
-
const user = users.find((u) => u[idFieldName] === ctxUser.sub);
|
|
6194
|
-
if (!user) {
|
|
6195
|
-
throw new Error("Not authorized");
|
|
6196
|
-
}
|
|
6197
|
-
user[passwordFieldName] = {
|
|
6198
|
-
value: password,
|
|
6199
|
-
passwordChangeRequired: false
|
|
6200
|
-
};
|
|
6201
|
-
const newBody = {};
|
|
6202
|
-
set(
|
|
6203
|
-
newBody,
|
|
6204
|
-
userField.path.slice(1),
|
|
6205
|
-
// remove _rawData from users path
|
|
6206
|
-
users.map((u) => {
|
|
6207
|
-
if (user[idFieldName] === u[idFieldName]) {
|
|
6208
|
-
return user;
|
|
6209
|
-
}
|
|
6210
|
-
return {
|
|
6211
|
-
// don't overwrite other users' passwords
|
|
6212
|
-
...u,
|
|
6213
|
-
[passwordFieldName]: {
|
|
6214
|
-
...u[passwordFieldName],
|
|
6215
|
-
value: ""
|
|
6216
|
-
}
|
|
6217
|
-
};
|
|
6218
|
-
})
|
|
6219
|
-
);
|
|
6220
|
-
await resolver.resolveUpdateDocument({
|
|
6221
|
-
collectionName: collection.name,
|
|
6222
|
-
relativePath,
|
|
6223
|
-
newBody
|
|
6224
|
-
});
|
|
6225
|
-
return true;
|
|
6226
|
-
}
|
|
6227
|
-
|
|
6228
6497
|
// src/error.ts
|
|
6229
6498
|
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
6230
6499
|
var NotFoundError = class extends GraphQLError3 {
|
|
@@ -6243,7 +6512,8 @@ var resolve = async ({
|
|
|
6243
6512
|
silenceErrors,
|
|
6244
6513
|
verbose,
|
|
6245
6514
|
isAudit,
|
|
6246
|
-
ctxUser
|
|
6515
|
+
ctxUser,
|
|
6516
|
+
isSignIn
|
|
6247
6517
|
}) => {
|
|
6248
6518
|
try {
|
|
6249
6519
|
const verboseValue = verbose ?? true;
|
|
@@ -6262,7 +6532,8 @@ var resolve = async ({
|
|
|
6262
6532
|
config,
|
|
6263
6533
|
database,
|
|
6264
6534
|
tinaSchema,
|
|
6265
|
-
isAudit: isAudit || false
|
|
6535
|
+
isAudit: isAudit || false,
|
|
6536
|
+
ctxUser
|
|
6266
6537
|
});
|
|
6267
6538
|
const resolverPromises = [];
|
|
6268
6539
|
const res = await graphql({
|
|
@@ -6306,7 +6577,7 @@ var resolve = async ({
|
|
|
6306
6577
|
sub: authArgs.sub,
|
|
6307
6578
|
password: authArgs.password,
|
|
6308
6579
|
info,
|
|
6309
|
-
|
|
6580
|
+
isSignIn
|
|
6310
6581
|
});
|
|
6311
6582
|
}
|
|
6312
6583
|
if (info.fieldName === "authorize") {
|
|
@@ -6650,6 +6921,8 @@ var handleCollectionField = (info, args, resolver) => {
|
|
|
6650
6921
|
import { ManyLevelGuest } from "many-level";
|
|
6651
6922
|
import { pipeline } from "readable-stream";
|
|
6652
6923
|
import { connect } from "net";
|
|
6924
|
+
var CONNECT_RETRIES = 50;
|
|
6925
|
+
var CONNECT_RETRY_DELAY_MS = 100;
|
|
6653
6926
|
var TinaLevelClient = class extends ManyLevelGuest {
|
|
6654
6927
|
port;
|
|
6655
6928
|
_connected = false;
|
|
@@ -6659,10 +6932,30 @@ var TinaLevelClient = class extends ManyLevelGuest {
|
|
|
6659
6932
|
}
|
|
6660
6933
|
openConnection() {
|
|
6661
6934
|
if (this._connected) return;
|
|
6662
|
-
const
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6935
|
+
const rpcStream = this.createRpcStream();
|
|
6936
|
+
const tryConnect = (attempt) => {
|
|
6937
|
+
const socket = connect(this.port);
|
|
6938
|
+
const onConnectError = (err) => {
|
|
6939
|
+
socket.destroy();
|
|
6940
|
+
if (attempt < CONNECT_RETRIES) {
|
|
6941
|
+
setTimeout(() => tryConnect(attempt + 1), CONNECT_RETRY_DELAY_MS);
|
|
6942
|
+
} else {
|
|
6943
|
+
console.error(
|
|
6944
|
+
`TinaLevelClient: could not connect to the datalayer server on port ${this.port} after ${attempt + 1} attempts (${err.message}).`
|
|
6945
|
+
);
|
|
6946
|
+
rpcStream.destroy(err);
|
|
6947
|
+
this._connected = false;
|
|
6948
|
+
}
|
|
6949
|
+
};
|
|
6950
|
+
socket.once("error", onConnectError);
|
|
6951
|
+
socket.once("connect", () => {
|
|
6952
|
+
socket.removeListener("error", onConnectError);
|
|
6953
|
+
pipeline(socket, rpcStream, socket, () => {
|
|
6954
|
+
this._connected = false;
|
|
6955
|
+
});
|
|
6956
|
+
});
|
|
6957
|
+
};
|
|
6958
|
+
tryConnect(0);
|
|
6666
6959
|
this._connected = true;
|
|
6667
6960
|
}
|
|
6668
6961
|
};
|
|
@@ -6672,7 +6965,11 @@ import path5 from "node:path";
|
|
|
6672
6965
|
import { GraphQLError as GraphQLError5 } from "graphql";
|
|
6673
6966
|
import micromatch2 from "micromatch";
|
|
6674
6967
|
import sha2 from "js-sha1";
|
|
6675
|
-
import { set
|
|
6968
|
+
import { set } from "es-toolkit/compat";
|
|
6969
|
+
var withUnauthenticatedWrites = (options) => ({
|
|
6970
|
+
...options,
|
|
6971
|
+
allowUnauthenticatedWrites: options?.allowUnauthenticatedWrites ?? true
|
|
6972
|
+
});
|
|
6676
6973
|
var createLocalDatabase = (config) => {
|
|
6677
6974
|
const level = new TinaLevelClient(config?.port);
|
|
6678
6975
|
level.openConnection();
|
|
@@ -6680,7 +6977,8 @@ var createLocalDatabase = (config) => {
|
|
|
6680
6977
|
return new Database({
|
|
6681
6978
|
bridge: fsBridge,
|
|
6682
6979
|
...config || {},
|
|
6683
|
-
level
|
|
6980
|
+
level,
|
|
6981
|
+
authCollection: withUnauthenticatedWrites(config?.authCollection)
|
|
6684
6982
|
});
|
|
6685
6983
|
};
|
|
6686
6984
|
var createDatabase = (config) => {
|
|
@@ -6721,7 +7019,8 @@ var createDatabaseInternal = (config) => {
|
|
|
6721
7019
|
return new Database({
|
|
6722
7020
|
...config,
|
|
6723
7021
|
bridge: config.bridge,
|
|
6724
|
-
level: config.level
|
|
7022
|
+
level: config.level,
|
|
7023
|
+
authCollection: withUnauthenticatedWrites(config.authCollection)
|
|
6725
7024
|
});
|
|
6726
7025
|
};
|
|
6727
7026
|
var SYSTEM_FILES = ["_schema", "_graphql", "_lookup"];
|
|
@@ -6740,6 +7039,10 @@ var Database = class {
|
|
|
6740
7039
|
this.onDelete = config.onDelete || defaultOnDelete;
|
|
6741
7040
|
this.levelBatchSize = config.levelBatchSize ?? DEFAULT_LEVEL_BATCH_SIZE;
|
|
6742
7041
|
this.contentNamespace = config.namespace;
|
|
7042
|
+
this.authCollection = Object.freeze({
|
|
7043
|
+
admins: Object.freeze([...config.authCollection?.admins ?? []]),
|
|
7044
|
+
allowUnauthenticatedWrites: config.authCollection?.allowUnauthenticatedWrites === true
|
|
7045
|
+
});
|
|
6743
7046
|
}
|
|
6744
7047
|
config;
|
|
6745
7048
|
bridge;
|
|
@@ -6751,6 +7054,7 @@ var Database = class {
|
|
|
6751
7054
|
onPut;
|
|
6752
7055
|
onDelete;
|
|
6753
7056
|
levelBatchSize;
|
|
7057
|
+
authCollection;
|
|
6754
7058
|
tinaSchema;
|
|
6755
7059
|
contentNamespace;
|
|
6756
7060
|
collectionIndexDefinitions;
|
|
@@ -6871,7 +7175,7 @@ var Database = class {
|
|
|
6871
7175
|
);
|
|
6872
7176
|
const indexDefinitions = await this.getIndexDefinitions(this.contentLevel);
|
|
6873
7177
|
const collectionIndexDefinitions = indexDefinitions?.[collection.name];
|
|
6874
|
-
const
|
|
7178
|
+
const referenceSchema = await this.getSchema();
|
|
6875
7179
|
const normalizedPath = normalizePath(filepath);
|
|
6876
7180
|
if (!collection?.isDetached) {
|
|
6877
7181
|
if (this.bridge) {
|
|
@@ -6903,7 +7207,7 @@ var Database = class {
|
|
|
6903
7207
|
...makeRefOpsForDocument(
|
|
6904
7208
|
normalizedPath,
|
|
6905
7209
|
collection?.name,
|
|
6906
|
-
|
|
7210
|
+
referenceSchema,
|
|
6907
7211
|
dataFields,
|
|
6908
7212
|
"put",
|
|
6909
7213
|
level
|
|
@@ -6934,7 +7238,7 @@ var Database = class {
|
|
|
6934
7238
|
...makeRefOpsForDocument(
|
|
6935
7239
|
normalizedPath,
|
|
6936
7240
|
collection?.name,
|
|
6937
|
-
|
|
7241
|
+
referenceSchema,
|
|
6938
7242
|
existingItem,
|
|
6939
7243
|
"del",
|
|
6940
7244
|
level
|
|
@@ -6986,7 +7290,7 @@ var Database = class {
|
|
|
6986
7290
|
);
|
|
6987
7291
|
collectionIndexDefinitions = indexDefinitions?.[collectionName];
|
|
6988
7292
|
}
|
|
6989
|
-
const
|
|
7293
|
+
const referenceSchema = await this.getSchema();
|
|
6990
7294
|
const normalizedPath = normalizePath(filepath);
|
|
6991
7295
|
const dataFields = await this.formatBodyOnPayload(filepath, data);
|
|
6992
7296
|
const collection = await this.collectionForPath(filepath);
|
|
@@ -7040,7 +7344,7 @@ var Database = class {
|
|
|
7040
7344
|
...makeRefOpsForDocument(
|
|
7041
7345
|
normalizedPath,
|
|
7042
7346
|
collectionName,
|
|
7043
|
-
|
|
7347
|
+
referenceSchema,
|
|
7044
7348
|
dataFields,
|
|
7045
7349
|
"put",
|
|
7046
7350
|
level
|
|
@@ -7071,7 +7375,7 @@ var Database = class {
|
|
|
7071
7375
|
...makeRefOpsForDocument(
|
|
7072
7376
|
normalizedPath,
|
|
7073
7377
|
collectionName,
|
|
7074
|
-
|
|
7378
|
+
referenceSchema,
|
|
7075
7379
|
existingItem,
|
|
7076
7380
|
"del",
|
|
7077
7381
|
level
|
|
@@ -7347,7 +7651,7 @@ var Database = class {
|
|
|
7347
7651
|
}
|
|
7348
7652
|
if (fields) {
|
|
7349
7653
|
for (const field of fields) {
|
|
7350
|
-
if (field.indexed !== void 0 && field.indexed === false || field.type === "object") {
|
|
7654
|
+
if (field.indexed !== void 0 && field.indexed === false || field.type === "password" || field.type === "object") {
|
|
7351
7655
|
continue;
|
|
7352
7656
|
}
|
|
7353
7657
|
indexDefinitions[field.name] = {
|
|
@@ -7497,32 +7801,41 @@ var Database = class {
|
|
|
7497
7801
|
endKey = key || "";
|
|
7498
7802
|
edges = [...edges, { cursor: key, path: filepath, value: itemRecord }];
|
|
7499
7803
|
}
|
|
7500
|
-
|
|
7501
|
-
edges
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
console.
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7804
|
+
const hydratedEdges = await sequential(
|
|
7805
|
+
edges,
|
|
7806
|
+
async ({
|
|
7807
|
+
cursor,
|
|
7808
|
+
path: path9,
|
|
7809
|
+
value
|
|
7810
|
+
}) => {
|
|
7811
|
+
try {
|
|
7812
|
+
const node = await hydrator(path9, value);
|
|
7813
|
+
return {
|
|
7814
|
+
node,
|
|
7815
|
+
cursor: btoa(cursor)
|
|
7816
|
+
};
|
|
7817
|
+
} catch (error) {
|
|
7818
|
+
if (error instanceof NotFoundError) {
|
|
7819
|
+
console.warn(
|
|
7820
|
+
`Skipping ${path9} in collection ${collection.name}: its index entry has no record. Reindex to remove it.`
|
|
7821
|
+
);
|
|
7822
|
+
return null;
|
|
7823
|
+
}
|
|
7824
|
+
console.log(error);
|
|
7825
|
+
if (error instanceof Error && (!path9.includes(".tina/__generated__/_graphql.json") || !path9.includes("tina/__generated__/_graphql.json"))) {
|
|
7826
|
+
throw new TinaQueryError({
|
|
7827
|
+
originalError: error,
|
|
7828
|
+
file: path9,
|
|
7829
|
+
collection: collection.name
|
|
7830
|
+
});
|
|
7524
7831
|
}
|
|
7832
|
+
throw error;
|
|
7525
7833
|
}
|
|
7834
|
+
}
|
|
7835
|
+
);
|
|
7836
|
+
return {
|
|
7837
|
+
edges: hydratedEdges.filter(
|
|
7838
|
+
(edge) => edge !== null
|
|
7526
7839
|
),
|
|
7527
7840
|
pageInfo: {
|
|
7528
7841
|
hasPreviousPage,
|
|
@@ -7688,7 +8001,7 @@ var Database = class {
|
|
|
7688
8001
|
throw new Error(`No collection found for path: ${filepath}`);
|
|
7689
8002
|
}
|
|
7690
8003
|
const indexDefinitions = await this.getIndexDefinitions(this.contentLevel);
|
|
7691
|
-
const
|
|
8004
|
+
const referenceSchema = await this.getSchema();
|
|
7692
8005
|
const collectionIndexDefinitions = indexDefinitions?.[collection.name];
|
|
7693
8006
|
let level = this.contentLevel;
|
|
7694
8007
|
if (collection?.isDetached) {
|
|
@@ -7710,7 +8023,7 @@ var Database = class {
|
|
|
7710
8023
|
...makeRefOpsForDocument(
|
|
7711
8024
|
normalizedPath,
|
|
7712
8025
|
collection.name,
|
|
7713
|
-
|
|
8026
|
+
referenceSchema,
|
|
7714
8027
|
item,
|
|
7715
8028
|
"del",
|
|
7716
8029
|
level
|
|
@@ -7812,7 +8125,7 @@ var hashPasswordVisitor = async (node, path9) => {
|
|
|
7812
8125
|
const passwordValuePath = [...path9, "value"];
|
|
7813
8126
|
const plaintextPassword = get(node, passwordValuePath);
|
|
7814
8127
|
if (plaintextPassword) {
|
|
7815
|
-
|
|
8128
|
+
set(
|
|
7816
8129
|
node,
|
|
7817
8130
|
passwordValuePath,
|
|
7818
8131
|
await generatePasswordHash({ password: plaintextPassword })
|
|
@@ -7857,7 +8170,6 @@ var _indexContent = async ({
|
|
|
7857
8170
|
}
|
|
7858
8171
|
collectionPath = collection.path;
|
|
7859
8172
|
}
|
|
7860
|
-
const collectionReferences = (await database.getCollectionReferences())?.[collection?.name];
|
|
7861
8173
|
const tinaSchema = await database.getSchema();
|
|
7862
8174
|
let templateInfo = null;
|
|
7863
8175
|
if (collection) {
|
|
@@ -7894,7 +8206,7 @@ var _indexContent = async ({
|
|
|
7894
8206
|
...makeRefOpsForDocument(
|
|
7895
8207
|
normalizedPath,
|
|
7896
8208
|
collection?.name,
|
|
7897
|
-
|
|
8209
|
+
tinaSchema,
|
|
7898
8210
|
item,
|
|
7899
8211
|
"del",
|
|
7900
8212
|
level
|
|
@@ -7930,7 +8242,7 @@ var _indexContent = async ({
|
|
|
7930
8242
|
...makeRefOpsForDocument(
|
|
7931
8243
|
normalizedPath,
|
|
7932
8244
|
collection?.name,
|
|
7933
|
-
|
|
8245
|
+
tinaSchema,
|
|
7934
8246
|
aliasedData,
|
|
7935
8247
|
"put",
|
|
7936
8248
|
level
|
|
@@ -8000,7 +8312,6 @@ var _deleteIndexContent = async (database, documentPaths, enqueueOps, collection
|
|
|
8000
8312
|
throw new Error(`No indexDefinitions for collection ${collection.name}`);
|
|
8001
8313
|
}
|
|
8002
8314
|
}
|
|
8003
|
-
const collectionReferences = (await database.getCollectionReferences())?.[collection?.name];
|
|
8004
8315
|
const tinaSchema = await database.getSchema();
|
|
8005
8316
|
let templateInfo = null;
|
|
8006
8317
|
if (collection) {
|
|
@@ -8027,7 +8338,7 @@ var _deleteIndexContent = async (database, documentPaths, enqueueOps, collection
|
|
|
8027
8338
|
...makeRefOpsForDocument(
|
|
8028
8339
|
itemKey,
|
|
8029
8340
|
collection?.name,
|
|
8030
|
-
|
|
8341
|
+
tinaSchema,
|
|
8031
8342
|
aliasedData,
|
|
8032
8343
|
"del",
|
|
8033
8344
|
database.contentLevel
|