convex-ents
Version:
Relations, default values, unique fields, RLS for Convex
1,566 lines (1,561 loc) • 47.3 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/functions.ts
var functions_exports = {};
__export(functions_exports, {
addEntRules: () => addEntRules,
entWrapper: () => entWrapper,
entsTableFactory: () => entsTableFactory,
getDeletionConfig: () => getDeletionConfig,
getReadRule: () => getReadRule,
getWriteRule: () => getWriteRule
});
module.exports = __toCommonJS(functions_exports);
// src/schema.ts
var import_server = require("convex/server");
var import_values = require("convex/values");
function edgeCompoundIndexName(edgeDefinition) {
return edgeCompoundIndexNameRaw(edgeDefinition.field, edgeDefinition.ref);
}
function edgeCompoundIndexNameRaw(idA, idB) {
return `${idA}_${idB}`;
}
// src/shared.ts
function getEdgeDefinitions(entDefinitions, table) {
return entDefinitions[table].edges;
}
// src/writer.ts
var import_server2 = require("convex/server");
var WriterImplBase = class _WriterImplBase {
constructor(ctx, entDefinitions, table) {
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
}
async deleteId(id, behavior) {
await this.checkReadAndWriteRule("delete", id, void 0);
const deletionConfig = getDeletionConfig(this.entDefinitions, this.table);
const isDeletingSoftly = behavior !== "hard" && deletionConfig !== void 0 && (deletionConfig.type === "soft" || deletionConfig.type === "scheduled");
if (behavior === "soft" && !isDeletingSoftly) {
throw new Error(
`Cannot soft delete document with ID "${id}" in table "${this.table}" because it does not have a "soft" or "scheduled" deletion behavior configured.`
);
}
const edges = {};
await Promise.all(
Object.values(getEdgeDefinitions(this.entDefinitions, this.table)).map(
async (edgeDefinition) => {
const key = edgeDefinition.name;
if (edgeDefinition.cardinality === "single" && edgeDefinition.type === "ref" || edgeDefinition.cardinality === "multiple" && edgeDefinition.type === "field") {
if (!isDeletingSoftly || edgeDefinition.deletion === "soft") {
const remove = (await this.ctx.db.query(edgeDefinition.to).withIndex(
edgeDefinition.ref,
(q) => q.eq(edgeDefinition.ref, id)
).collect()).map((doc) => doc._id);
edges[key] = { remove };
}
} else if (edgeDefinition.cardinality === "single") {
if (edgeDefinition.deletion !== void 0 && (!isDeletingSoftly || edgeDefinition.deletion === "soft")) {
const doc = await this.ctx.db.get(id);
if (doc !== null) {
const otherId = doc[edgeDefinition.field];
edges[key] = {
remove: otherId !== void 0 ? [otherId] : []
};
}
}
} else if (edgeDefinition.cardinality === "multiple") {
if (!isDeletingSoftly) {
const removeEdges = (await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeDefinition.field,
(q) => q.eq(edgeDefinition.field, id)
).collect()).concat(
edgeDefinition.symmetric ? await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeDefinition.ref,
(q) => q.eq(edgeDefinition.ref, id)
).collect() : []
).map((doc) => doc._id);
edges[key] = { removeEdges };
}
}
}
)
);
const deletionTime = +/* @__PURE__ */ new Date();
if (isDeletingSoftly) {
await this.ctx.db.patch(this.table, id, { deletionTime });
} else {
try {
await this.ctx.db.delete(this.table, id);
} catch {
}
}
await this.writeEdges(id, edges, isDeletingSoftly);
if (deletionConfig !== void 0 && deletionConfig.type === "scheduled") {
const fnRef = this.ctx.scheduledDelete ?? (0, import_server2.makeFunctionReference)(
"functions:scheduledDelete"
);
await this.ctx.scheduler.runAfter(deletionConfig.delayMs ?? 0, fnRef, {
origin: {
id,
table: this.table,
deletionTime
},
inProgress: false,
stack: []
});
}
return id;
}
async deleteIdIn(id, table, cascadingSoft) {
await new _WriterImplBase(this.ctx, this.entDefinitions, table).deleteId(
id,
cascadingSoft ? "soft" : "hard"
);
}
async deleteSystem(table, id) {
switch (table) {
case "_storage":
await this.ctx.storage.delete(id);
break;
case "_scheduled_functions":
await this.ctx.scheduler.cancel(id);
break;
default:
throw new Error(
`Cannot cascade deletion to unsupported system table "${table}".`
);
}
}
async writeEdges(docId, changes, deleteSoftly) {
await Promise.all(
Object.values(getEdgeDefinitions(this.entDefinitions, this.table)).map(
async (edgeDefinition) => {
const idOrIds = changes[edgeDefinition.name];
if (idOrIds === void 0) {
return;
}
if (edgeDefinition.cardinality === "single" && edgeDefinition.type === "ref" || edgeDefinition.cardinality === "multiple" && edgeDefinition.type === "field") {
if (idOrIds.remove !== void 0 && idOrIds.remove.length > 0) {
await Promise.all(
idOrIds.remove.map(
(id) => this.deleteIdIn(
id,
edgeDefinition.to,
(deleteSoftly ?? false) && edgeDefinition.deletion === "soft"
)
)
);
}
if (idOrIds.add !== void 0 && idOrIds.add.length > 0) {
await Promise.all(
idOrIds.add.map(
async (id) => this.ctx.db.patch(edgeDefinition.to, id, {
[edgeDefinition.ref]: docId
})
)
);
}
} else if (edgeDefinition.cardinality === "single") {
if (idOrIds.remove !== void 0 && idOrIds.remove.length > 0) {
await Promise.all(
idOrIds.remove.map(
isSystemTable(edgeDefinition.to) ? (id) => this.deleteSystem(edgeDefinition.to, id) : (id) => this.deleteIdIn(
id,
edgeDefinition.to,
(deleteSoftly ?? false) && edgeDefinition.deletion === "soft"
)
)
);
}
} else if (edgeDefinition.cardinality === "multiple") {
if ((idOrIds.removeEdges ?? []).length > 0) {
await Promise.all(
idOrIds.removeEdges.map(async (id) => {
try {
await this.ctx.db.delete(edgeDefinition.table, id);
} catch {
}
})
);
}
if (idOrIds.add !== void 0) {
await Promise.all(
[...new Set(idOrIds.add)].map(async (id) => {
const existing = await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeCompoundIndexName(edgeDefinition),
(q) => q.eq(edgeDefinition.field, docId).eq(
edgeDefinition.ref,
id
)
).first();
if (existing === null) {
await this.ctx.db.insert(edgeDefinition.table, {
[edgeDefinition.field]: docId,
[edgeDefinition.ref]: id
});
if (edgeDefinition.symmetric) {
await this.ctx.db.insert(edgeDefinition.table, {
[edgeDefinition.field]: id,
[edgeDefinition.ref]: docId
});
}
}
})
);
}
}
}
)
);
}
async checkUniqueness(value, id) {
await Promise.all(
Object.values(
this.entDefinitions[this.table].fields
).map(async (fieldDefinition) => {
if (fieldDefinition.unique) {
const key = fieldDefinition.name;
const fieldValue = value[key];
const existing = await this.ctx.db.query(this.table).withIndex(key, (q) => q.eq(key, value[key])).unique();
if (existing !== null && (id === void 0 || existing._id !== id)) {
throw new Error(
`In table "${this.table}" cannot create a duplicate document with field "${key}" of value \`${fieldValue}\`, existing document with ID "${existing._id}" already has it.`
);
}
}
})
);
await Promise.all(
Object.values(getEdgeDefinitions(this.entDefinitions, this.table)).map(
async (edgeDefinition) => {
if (edgeDefinition.cardinality === "single" && edgeDefinition.type === "field" && edgeDefinition.unique) {
const key = edgeDefinition.field;
if (value[key] === void 0) {
return;
}
const existing = await this.ctx.db.query(this.table).withIndex(key, (q) => q.eq(key, value[key])).unique();
if (existing !== null && (id === void 0 || existing._id !== id)) {
throw new Error(
`In table "${this.table}" cannot create a duplicate 1:1 edge "${edgeDefinition.name}" to ID "${value[key]}", existing document with ID "${existing._id}" already has it.`
);
}
}
}
)
);
}
fieldsOnly(value) {
const fields = {};
Object.keys(value).forEach((key) => {
const edgeDefinition = getEdgeDefinitions(
this.entDefinitions,
this.table
)[key];
if (edgeDefinition === void 0 || edgeDefinition.cardinality === "single" && edgeDefinition.type === "field" && edgeDefinition.field === key) {
fields[key] = value[key];
}
});
return fields;
}
async checkReadAndWriteRule(operation, id, value) {
if (id !== void 0) {
const readPolicy = getReadRule(this.entDefinitions, this.table);
if (readPolicy !== void 0) {
const doc = await this.ctx.db.get(id);
if (doc === null) {
throw new Error(
`Cannot update document with ID "${id}" in table "${this.table} because it does not exist"`
);
}
const decision2 = await readPolicy(doc);
if (!decision2) {
throw new Error(
`Cannot update document with ID "${id}" from table "${this.table}"`
);
}
}
}
const writePolicy = getWriteRule(this.entDefinitions, this.table);
if (writePolicy === void 0) {
return;
}
const ent = id === void 0 ? void 0 : entWrapper(
await this.ctx.db.get(id),
this.ctx,
this.entDefinitions,
this.table
);
const { _id, _creationTime, ...safeValue } = value ?? {};
const decision = await writePolicy({
operation,
ent,
value: value !== void 0 ? safeValue : void 0
});
if (!decision) {
if (id === void 0) {
throw new Error(
`Cannot insert into table "${this.table}": \`${JSON.stringify(
value
)}\``
);
} else if (value === void 0) {
throw new Error(
`Cannot delete from table "${this.table}" with ID "${id}"`
);
} else {
throw new Error(
`Cannot update document with ID "${id}" in table "${this.table}" with: \`${JSON.stringify(value)}\``
);
}
}
}
};
function isSystemTable(table) {
return table.startsWith("_");
}
// src/functions.ts
var PromiseQueryOrNullImpl = class _PromiseQueryOrNullImpl extends Promise {
constructor(ctx, entDefinitions, table, retrieve) {
super(() => {
});
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
this.retrieve = retrieve;
}
filter(predicate) {
return new _PromiseQueryOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const query = await this.retrieve();
if (query === null) {
return null;
}
return query.filter(predicate);
}
);
}
map(callbackFn) {
return new PromiseArrayImpl(async () => {
const array = await this;
if (array === null) {
return null;
}
return await Promise.all(array.map(callbackFn));
});
}
order(order, indexName) {
return new _PromiseQueryOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const query = await this.retrieve();
if (query === null) {
return null;
}
if (indexName !== void 0) {
return query.withIndex(indexName).order(order);
}
return query.order(order);
}
);
}
paginate(paginationOpts) {
return new PromisePaginationResultOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const query = await this.retrieve();
if (query === null) {
return null;
}
return await query.paginate(paginationOpts);
}
);
}
take(n) {
return new PromiseEntsOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
return await this._take(n);
},
false
);
}
first() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(1);
if (docs === null) {
return nullRetriever;
}
const [doc] = docs;
return loadedRetriever(doc);
},
false
);
}
firstX() {
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(1);
if (docs === null) {
return nullRetriever;
}
const [doc] = docs;
if (doc === void 0) {
throw new Error("Query returned no documents");
}
return loadedRetriever(doc);
},
false
);
}
unique() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(2);
if (docs === null) {
return nullRetriever;
}
if (docs.length === 0) {
return nullRetriever;
}
if (docs.length === 2) {
throw new Error("unique() query returned more than one result");
}
const [doc] = docs;
return loadedRetriever(doc);
},
false
);
}
uniqueX() {
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(2);
if (docs === null) {
return nullRetriever;
}
if (docs.length === 0) {
throw new Error("Query returned no documents");
}
if (docs.length === 2) {
throw new Error("unique() query returned more than one result");
}
const [doc] = docs;
return loadedRetriever(doc);
},
true
);
}
async docs() {
const query = await this.retrieve();
if (query === null) {
return null;
}
const docs = await query.collect();
return filterByReadRule(
this.ctx,
this.entDefinitions,
this.table,
docs,
false
);
}
then(onfulfilled, onrejected) {
return this.docs().then(
(documents) => documents === null ? null : documents.map(
(doc) => entWrapper(doc, this.ctx, this.entDefinitions, this.table)
)
).then(onfulfilled, onrejected);
}
async _take(n) {
const query = await this.retrieve();
return await takeFromQuery(
query,
n,
this.ctx,
this.entDefinitions,
this.table
);
}
};
var PromisePaginationResultOrNullImpl = class extends Promise {
constructor(ctx, entDefinitions, table, retrieve) {
super(() => {
});
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
this.retrieve = retrieve;
}
async map(callbackFn) {
const result = await this;
if (result === null) {
return null;
}
return {
...result,
page: await Promise.all(result.page.map(callbackFn))
};
}
async docs() {
const result = await this.retrieve();
if (result === null) {
return null;
}
return {
...result,
page: await filterByReadRule(
this.ctx,
this.entDefinitions,
this.table,
result.page,
false
)
};
}
then(onfulfilled, onrejected) {
return this.docs().then(
(result) => result === null ? null : {
...result,
page: result.page.map(
(doc) => entWrapper(doc, this.ctx, this.entDefinitions, this.table)
)
}
).then(onfulfilled, onrejected);
}
};
var PromiseTableImpl = class extends PromiseQueryOrNullImpl {
constructor(ctx, entDefinitions, table) {
super(
ctx,
entDefinitions,
table,
async () => isSystemTable(table) ? ctx.db.system.query(table) : ctx.db.query(table)
);
}
get(...args) {
return this.getImpl(args);
}
getX(...args) {
return this.getImpl(args, true);
}
getMany(...args) {
return this.getManyImpl(args);
}
getManyX(...args) {
return this.getManyImpl(args, true);
}
getImpl(args, throwIfNull = false) {
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
this.table,
args.length === 1 ? async () => {
const id = args[0];
if (this.normalizeId(id) === null) {
throw new Error(`Invalid id \`${id}\` for table "${this.table}"`);
}
return {
id,
doc: async () => {
const doc = await (isSystemTable(this.table) ? this.ctx.db.system.get(id) : this.ctx.db.get(id));
if (throwIfNull && doc === null) {
throw new Error(
`Document not found with id \`${id}\` in table "${this.table}"`
);
}
return doc;
}
};
} : async () => {
const [indexName, ...values] = args;
const fieldNames = getIndexFields(
this.entDefinitions,
this.table,
indexName
);
const doc = await this.ctx.db.query(this.table).withIndex(
indexName,
(q) => values.reduce((q2, value, i) => q2.eq(fieldNames[i], value), q)
).unique();
if (throwIfNull && doc === null) {
throw new Error(
`Table "${this.table}" does not contain document with field${values.reduce(
(message, value, i) => `${message} "${fieldNames[i]}" = \`${value}\``,
""
)}`
);
}
return loadedRetriever(doc);
},
throwIfNull
);
}
getManyImpl(args, throwIfNull = false) {
return new PromiseEntsOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
args.length === 1 ? async () => {
const ids = args[0];
ids.forEach((id) => {
if (this.normalizeId(id) === null) {
throw new Error(
`Invalid id \`${id}\` for table "${this.table}"`
);
}
});
return await Promise.all(
ids.map(async (id) => {
const doc = await (isSystemTable(this.table) ? this.ctx.db.system.get(id) : this.ctx.db.get(id));
if (throwIfNull && doc === null) {
throw new Error(
`Document not found with id \`${id}\` in table "${this.table}"`
);
}
return doc;
})
);
} : async () => {
const [indexName, values] = args;
return await Promise.all(
values.map(async (value) => {
const doc = await this.ctx.db.query(this.table).withIndex(indexName, (q) => q.eq(indexName, value)).unique();
if (throwIfNull && doc === null) {
throw new Error(
`Table "${this.table}" does not contain document with field "${indexName}" = \`${value}\``
);
}
return doc;
})
);
},
throwIfNull
);
}
normalizeId(id) {
return isSystemTable(this.table) ? this.ctx.db.system.normalizeId(this.table, id) : this.ctx.db.normalizeId(this.table, id);
}
// normalizeId or throw
normalizeIdX(id) {
const normalized = this.normalizeId(id);
if (normalized === null) {
throw new Error(`Invalid id \`${id}\` for table "${this.table}"`);
}
return normalized;
}
withIndex(indexName, indexRange) {
return new PromiseQueryOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const query = await this.retrieve();
return query.withIndex(indexName, indexRange);
}
);
}
search(indexName, searchFilter) {
return new PromiseQueryOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const query = await this.retrieve();
return query.withSearchIndex(indexName, searchFilter);
}
);
}
};
var PromiseEntsOrNullImpl = class extends Promise {
constructor(ctx, entDefinitions, table, retrieve, throwIfNull) {
super(() => {
});
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
this.retrieve = retrieve;
this.throwIfNull = throwIfNull;
}
map(callbackFn) {
return new PromiseArrayImpl(async () => {
const array = await this;
if (array === null) {
return null;
}
return await Promise.all(array.map(callbackFn));
});
}
first() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this.retrieve();
if (docs === null) {
return nullRetriever;
}
return loadedRetriever(docs[0] ?? null);
},
false
);
}
firstX() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this.retrieve();
if (docs === null) {
return nullRetriever;
}
const doc = docs[0] ?? void 0;
if (doc === void 0) {
throw new Error("Query returned no documents");
}
return loadedRetriever(doc);
},
true
);
}
unique() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this.retrieve();
if (docs === null) {
return nullRetriever;
}
if (docs.length > 1) {
throw new Error("unique() query returned more than one result");
}
return loadedRetriever(docs[0] ?? null);
},
false
);
}
uniqueX() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this.retrieve();
if (docs === null) {
return nullRetriever;
}
if (docs.length > 1) {
throw new Error("unique() query returned more than one result");
}
if (docs.length < 1) {
throw new Error("unique() query returned no documents");
}
return loadedRetriever(docs[0]);
},
true
);
}
async docs() {
const docs = await this.retrieve();
return filterByReadRule(
this.ctx,
this.entDefinitions,
this.table,
docs,
this.throwIfNull
);
}
then(onfulfilled, onrejected) {
return this.docs().then(
(docs) => (
// Handles PromiseEntsOrNulls
docs === null ? null : docs.map(
(doc) => doc === null ? null : entWrapper(doc, this.ctx, this.entDefinitions, this.table)
)
)
).then(onfulfilled, onrejected);
}
};
var PromiseEdgeOrNullImpl = class _PromiseEdgeOrNullImpl extends PromiseEntsOrNullImpl {
constructor(ctx, entDefinitions, table, edgeDefinition, retrieveSourceId, retrieveQuery, retrieveDoc = async (edgeDoc) => {
const sourceId = edgeDoc[edgeDefinition.field];
const targetId = edgeDoc[edgeDefinition.ref];
const doc = await this.ctx.db.get(targetId);
if (doc === null) {
throw new Error(
`Dangling reference for edge "${edgeDefinition.name}" in table "${this.table}" for document with ID "${sourceId}": Could not find a document with ID "${targetId}" in table "${edgeDefinition.to}" (edge document ID is "${edgeDoc._id}").`
);
}
return doc;
}) {
super(
ctx,
entDefinitions,
table,
async () => {
const query = await retrieveQuery();
if (query === null) {
return null;
}
const edgeDocs = await query.collect();
return await Promise.all(edgeDocs.map(retrieveDoc));
},
false
);
this.edgeDefinition = edgeDefinition;
this.retrieveSourceId = retrieveSourceId;
this.retrieveQuery = retrieveQuery;
this.retrieveDoc = retrieveDoc;
}
async has(targetId) {
const sourceId = await this.retrieveSourceId();
if (sourceId === null) {
return null;
}
const edgeDoc = await this.ctx.db.query(this.edgeDefinition.table).withIndex(
edgeCompoundIndexName(this.edgeDefinition),
(q) => q.eq(this.edgeDefinition.field, sourceId).eq(
this.edgeDefinition.ref,
targetId
)
).first();
return edgeDoc !== null;
}
order(order) {
return new _PromiseEdgeOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
this.edgeDefinition,
this.retrieveSourceId,
async () => {
const query = await this.retrieveQuery();
if (query === null) {
return null;
}
return query.order(order);
}
);
}
paginate(paginationOpts) {
return new PromisePaginationResultOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const query = await this.retrieveQuery();
if (query === null) {
return null;
}
const result = await query.paginate(paginationOpts);
return {
...result,
page: await Promise.all(result.page.map(this.retrieveDoc))
};
}
);
}
take(n) {
return new PromiseEntsOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
return await this._take(n);
},
false
);
}
first() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(1);
if (docs === null) {
return nullRetriever;
}
const [doc] = docs;
return loadedRetriever(doc);
},
false
);
}
firstX() {
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(1);
if (docs === null) {
return nullRetriever;
}
const [doc] = docs;
if (doc === void 0) {
throw new Error("Query returned no documents");
}
return loadedRetriever(doc);
},
false
);
}
unique() {
return new PromiseEntOrNullImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(2);
if (docs === null) {
return nullRetriever;
}
if (docs.length === 0) {
return nullRetriever;
}
if (docs.length === 2) {
throw new Error("unique() query returned more than one result");
}
const [doc] = docs;
return loadedRetriever(doc);
},
false
);
}
uniqueX() {
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const docs = await this._take(2);
if (docs === null) {
return nullRetriever;
}
if (docs.length === 0) {
throw new Error("Query returned no documents");
}
if (docs.length === 2) {
throw new Error("unique() query returned more than one result");
}
const [doc] = docs;
return loadedRetriever(doc);
},
true
);
}
async _take(n) {
const query = await this.retrieveQuery();
return await takeFromQuery(
query,
n,
this.ctx,
this.entDefinitions,
this.table,
this.retrieveDoc
);
}
};
var PromiseEntOrNullImpl = class extends Promise {
constructor(ctx, entDefinitions, table, retrieve, throwIfNull) {
super(() => {
});
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
this.retrieve = retrieve;
this.throwIfNull = throwIfNull;
}
async doc() {
const { id, doc: getDoc } = await this.retrieve();
if (id === null) {
return null;
}
const doc = await getDoc();
if (doc === null) {
return null;
}
const readPolicy = getReadRule(this.entDefinitions, this.table);
if (readPolicy !== void 0) {
const decision = await readPolicy(
entWrapper(doc, this.ctx, this.entDefinitions, this.table)
);
if (this.throwIfNull && !decision) {
throw new Error(
`Document cannot be read with id \`${doc._id}\` in table "${this.table}"`
);
}
return decision ? doc : null;
}
return doc;
}
then(onfulfilled, onrejected) {
return this.doc().then(
(doc) => doc === null ? null : entWrapper(doc, this.ctx, this.entDefinitions, this.table)
).then(onfulfilled, onrejected);
}
edge(edge) {
return this.edgeImpl(edge);
}
edgeX(edge) {
return this.edgeImpl(edge, true);
}
edgeImpl(edge, throwIfNull = false) {
const edgeDefinition = getEdgeDefinitions(this.entDefinitions, this.table)[edge];
if (edgeDefinition.cardinality === "multiple") {
if (edgeDefinition.type === "ref") {
return new PromiseEdgeOrNullImpl(
this.ctx,
this.entDefinitions,
edgeDefinition.to,
edgeDefinition,
async () => {
const { id } = await this.retrieve();
return id;
},
async () => {
const { id } = await this.retrieve();
if (id === null) {
return null;
}
return this.ctx.db.query(edgeDefinition.table).withIndex(
edgeDefinition.field,
(q) => q.eq(edgeDefinition.field, id)
);
}
);
}
return new PromiseQueryOrNullImpl(
this.ctx,
this.entDefinitions,
edgeDefinition.to,
async () => {
const { id } = await this.retrieve();
if (id === null) {
return null;
}
return this.ctx.db.query(edgeDefinition.to).withIndex(
edgeDefinition.ref,
(q) => q.eq(edgeDefinition.ref, id)
);
}
);
}
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
edgeDefinition.to,
async () => {
const { id, doc: getDoc } = await this.retrieve();
if (id === null) {
return nullRetriever;
}
if (edgeDefinition.type === "ref") {
const otherDoc = await this.ctx.db.query(edgeDefinition.to).withIndex(
edgeDefinition.ref,
(q) => q.eq(edgeDefinition.ref, id)
).unique();
if (throwIfNull && otherDoc === null) {
throw new Error(
`Edge "${edgeDefinition.name}" does not exist for document with ID "${id}"`
);
}
return loadedRetriever(otherDoc);
}
const doc = await getDoc();
const otherId = doc[edgeDefinition.field];
return {
id: otherId,
doc: async () => {
if (otherId === void 0) {
if (edgeDefinition.optional) {
return null;
}
throw new Error(
`Unexpected null reference for edge "${edgeDefinition.name}" in table "${this.table}" on document with ID "${id}": Expected an ID for a document in table "${edgeDefinition.to}".`
);
}
const otherDoc = await this.ctx.db.get(otherId);
if (otherDoc === null && edgeDefinition.to !== "_scheduled_functions") {
throw new Error(
`Dangling reference for edge "${edgeDefinition.name}" in table "${this.table}" on document with ID "${id}": Could not find a document with ID "${otherId}" in table "${edgeDefinition.to}".`
);
}
return otherDoc;
}
};
},
throwIfNull
);
}
};
var PromiseArrayImpl = class extends Promise {
constructor(retrieve) {
super(() => {
});
this.retrieve = retrieve;
}
async filter(predicate) {
const array = await this.retrieve();
if (array === null) {
return null;
}
return array.filter(predicate);
}
then(onfulfilled, onrejected) {
return this.retrieve().then(onfulfilled, onrejected);
}
};
function entWrapper(fields, ctx, entDefinitions, table) {
const doc = { ...fields };
const queryInterface = new PromiseEntWriterImpl(
ctx,
entDefinitions,
table,
async () => ({ id: doc._id, doc: async () => doc }),
// this `true` doesn't matter, the queryInterface cannot be awaited
true
);
Object.defineProperty(doc, "edge", {
value: (edge) => {
return queryInterface.edge(edge);
},
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(doc, "edgeX", {
value: (edge) => {
return queryInterface.edgeX(edge);
},
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(doc, "_edges", {
value: () => {
return getEdgeDefinitions(entDefinitions, table);
},
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(doc, "doc", {
value: () => {
return doc;
},
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(doc, "patch", {
value: (value) => {
return queryInterface.patch(value);
},
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(doc, "replace", {
value: (value) => {
return queryInterface.replace(value);
},
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(doc, "delete", {
value: () => {
return queryInterface.delete();
},
enumerable: false,
writable: false,
configurable: false
});
Object.entries(entDefinitions[table]?.defaults ?? []).map(
([field, value]) => {
if (doc[field] === void 0) {
doc[field] = value;
}
}
);
return doc;
}
function entsTableFactory(ctx, entDefinitions, options) {
const enrichedCtx = options !== void 0 ? { ...ctx, ...options } : ctx;
const table = (table2, indexName, indexRange) => {
if (typeof table2 !== "string") {
throw new Error(`Expected table name, got \`${table2}\``);
}
if (indexName !== void 0) {
return new PromiseTableImpl(
enrichedCtx,
entDefinitions,
table2
).withIndex(indexName, indexRange);
}
if (ctx.db.insert !== void 0) {
return new PromiseTableWriterImpl(
enrichedCtx,
entDefinitions,
table2
);
}
return new PromiseTableImpl(enrichedCtx, entDefinitions, table2);
};
table.system = table;
return table;
}
var PromiseTableWriterImpl = class extends PromiseTableImpl {
constructor(ctx, entDefinitions, table) {
super(ctx, entDefinitions, table);
this.ctx = ctx;
this.base = new WriterImplBase(ctx, entDefinitions, table);
}
base;
insert(value) {
return new PromiseEntIdImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
await this.base.checkReadAndWriteRule("create", void 0, value);
await this.base.checkUniqueness(value);
const fields = this.base.fieldsOnly(value);
const docId = await this.ctx.db.insert(this.table, fields);
const edges = {};
Object.keys(value).forEach((key) => {
const edgeDefinition = getEdgeDefinitions(
this.entDefinitions,
this.table
)[key];
if (edgeDefinition === void 0 || edgeDefinition.cardinality === "single" && edgeDefinition.type === "field") {
return;
}
edges[key] = {
add: edgeDefinition.cardinality === "single" ? [value[key]] : value[key]
};
});
await this.base.writeEdges(docId, edges);
return docId;
}
);
}
// TODO: fluent API
async insertMany(values) {
return await Promise.all(values.map((value) => this.insert(value)));
}
};
var PromiseEntWriterImpl = class extends PromiseEntOrNullImpl {
constructor(ctx, entDefinitions, table, retrieve, throwIfNull) {
super(ctx, entDefinitions, table, retrieve, throwIfNull);
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
this.retrieve = retrieve;
this.throwIfNull = throwIfNull;
this.base = new WriterImplBase(ctx, entDefinitions, table);
}
base;
patch(value) {
return new PromiseEntIdImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const { id: docId } = await this.retrieve();
const id = docId;
await this.base.checkReadAndWriteRule("update", id, value);
await this.base.checkUniqueness(value, id);
const fields = this.base.fieldsOnly(value);
if (Object.keys(fields).length > 0) {
await this.ctx.db.patch(this.table, id, fields);
}
const edges = {};
await Promise.all(
Object.keys(value).map(async (key) => {
const edgeDefinition = getEdgeDefinitions(
this.entDefinitions,
this.table
)[key];
if (edgeDefinition === void 0 || edgeDefinition.cardinality === "single" && edgeDefinition.type === "field") {
return;
}
if (edgeDefinition.cardinality === "single") {
throw new Error(
`Cannot set 1:1 edge "${edgeDefinition.name}" on ent in table "${this.table}", update the ent in "${edgeDefinition.to}" table instead.`
);
} else {
if (edgeDefinition.type === "field") {
throw new Error(
`Cannot set 1:many edges "${edgeDefinition.name}" on ent in table "${this.table}", update the ents in "${edgeDefinition.to}" table instead.`
);
} else {
const { add, remove } = value[key];
const removeEdges = (await Promise.all(
(remove ?? []).map(
async (otherId) => (await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeCompoundIndexName(edgeDefinition),
(q) => q.eq(edgeDefinition.field, id).eq(
edgeDefinition.ref,
otherId
)
).collect()).concat(
edgeDefinition.symmetric ? await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeCompoundIndexName(edgeDefinition),
(q) => q.eq(
edgeDefinition.field,
otherId
).eq(edgeDefinition.ref, id)
).collect() : []
)
)
)).flat().map((edgeDoc) => edgeDoc._id);
edges[key] = {
add,
removeEdges
};
}
}
})
);
await this.base.writeEdges(id, edges);
return id;
}
);
}
replace(value) {
return new PromiseEntIdImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const { id } = await this.retrieve();
const docId = id;
await this.base.checkReadAndWriteRule("update", docId, value);
await this.base.checkUniqueness(value, docId);
const fields = this.base.fieldsOnly(value);
await this.ctx.db.replace(this.table, docId, fields);
const edges = {};
await Promise.all(
Object.values(
getEdgeDefinitions(this.entDefinitions, this.table)
).map(async (edgeDefinition) => {
const key = edgeDefinition.name;
const idOrIds = value[key];
if (idOrIds === void 0) {
return;
}
if (edgeDefinition.cardinality === "single") {
if (edgeDefinition.type === "ref") {
const oldDoc = await this.ctx.db.get(docId);
if (oldDoc[key] !== void 0 && oldDoc[key] !== idOrIds) {
throw new Error("Cannot set 1:1 edge from ref end.");
}
}
} else {
if (edgeDefinition.type === "field") {
throw new Error("Cannot set 1:many edge from many end.");
} else {
const requested = new Set(idOrIds);
const removeEdges = (await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeDefinition.field,
(q) => q.eq(edgeDefinition.field, docId)
).collect()).map((doc) => [doc._id, doc[edgeDefinition.ref]]).concat(
edgeDefinition.symmetric ? (await this.ctx.db.query(edgeDefinition.table).withIndex(
edgeDefinition.ref,
(q) => q.eq(edgeDefinition.ref, docId)
).collect()).map(
(doc) => [doc._id, doc[edgeDefinition.field]]
) : []
).filter(([_edgeId, otherId]) => {
if (requested.has(otherId)) {
requested.delete(otherId);
return false;
}
return true;
}).map(([edgeId]) => edgeId);
edges[key] = {
add: idOrIds,
removeEdges
};
}
}
})
);
await this.base.writeEdges(docId, edges);
return docId;
}
);
}
async delete() {
const { id: docId } = await this.retrieve();
const id = docId;
return this.base.deleteId(id, "default");
}
};
var PromiseEntIdImpl = class extends Promise {
constructor(ctx, entDefinitions, table, retrieve) {
super(() => {
});
this.ctx = ctx;
this.entDefinitions = entDefinitions;
this.table = table;
this.retrieve = retrieve;
}
get() {
return new PromiseEntWriterImpl(
this.ctx,
this.entDefinitions,
this.table,
async () => {
const id = await this.retrieve();
return { id, doc: async () => this.ctx.db.get(id) };
},
true
);
}
then(onfulfilled, onrejected) {
return this.retrieve().then(onfulfilled, onrejected);
}
};
var nullRetriever = {
id: null,
doc: async () => null
};
function loadedRetriever(doc) {
return {
id: doc?._id ?? null,
doc: async () => doc
};
}
function addEntRules(entDefinitions, rules) {
return { ...entDefinitions, rules };
}
async function takeFromQuery(query, n, ctx, entDefinitions, table, mapToResult) {
if (query === null) {
return null;
}
const readPolicy = getReadRule(entDefinitions, table);
if (readPolicy === void 0) {
const results = await query.take(n);
if (mapToResult === void 0) {
return results;
}
return Promise.all(results.map(mapToResult));
}
let numItems = n;
const docs = [];
let hasMore = true;
const iterator = query[Symbol.asyncIterator]();
while (hasMore && docs.length < n) {
const page = [];
for (let i = 0; i < numItems; i++) {
const { done, value } = await iterator.next();
if (done) {
hasMore = false;
break;
}
page.push(mapToResult === void 0 ? value : await mapToResult(value));
}
docs.push(
...(await filterByReadRule(
ctx,
entDefinitions,
table,
page,
false
)).slice(0, n - docs.length)
);
numItems = Math.min(64, numItems * 2);
}
return docs;
}
async function filterByReadRule(ctx, entDefinitions, table, docs, throwIfNull) {
if (docs === null) {
return null;
}
const readPolicy = getReadRule(entDefinitions, table);
if (readPolicy === void 0) {
return docs;
}
const decisions = await Promise.all(
docs.map(async (doc) => {
const decision = await readPolicy(
entWrapper(doc, ctx, entDefinitions, table)
);
if (throwIfNull && !decision) {
throw new Error(
`Document cannot be read with id \`${doc._id}\` in table "${table}"`
);
}
return decision;
})
);
return docs.filter((_, i) => decisions[i]);
}
function getIndexFields(entDefinitions, table, index) {
return entDefinitions[table].indexes[index];
}
function getReadRule(entDefinitions, table) {
return entDefinitions.rules?.[table]?.read;
}
function getWriteRule(entDefinitions, table) {
return entDefinitions.rules?.[table]?.write;
}
function getDeletionConfig(entDefinitions, table) {
return entDefinitions[table].deletionConfig;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
addEntRules,
entWrapper,
entsTableFactory,
getDeletionConfig,
getReadRule,
getWriteRule
});
//# sourceMappingURL=functions.js.map