Files
outline/server/commands/documentCreator.ts
T
Tom Moor 0b213bd6b8 feat: Map document creator to existing users during JSON import (#11879)
* feat: Map creator/updater IDs to existing users during JSON import

When importing documents from JSON, resolve the original document author
to an internal user by matching on user ID first, then email, falling
back to the importing user. Results are cached to avoid redundant queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Add negative caching for user resolution during import

Cache misses (not just hits) in resolveUserId so that repeated lookups
for users that don't exist in the target team are served from cache
instead of hitting the database for every document.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: Fix resolveUserId JSDoc to match actual behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 08:42:32 -04:00

165 lines
3.6 KiB
TypeScript

import type { Optional } from "utility-types";
import { TextHelper } from "@shared/utils/TextHelper";
import { Document, type Template } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import type { APIContext } from "@server/types";
type Props = Optional<
Pick<
Document,
| "id"
| "urlId"
| "title"
| "text"
| "content"
| "icon"
| "color"
| "collectionId"
| "parentDocumentId"
| "importId"
| "apiImportId"
| "fullWidth"
| "sourceMetadata"
| "editorVersion"
| "publishedAt"
| "createdAt"
| "updatedAt"
| "createdById"
| "lastModifiedById"
>
> & {
state?: Buffer;
publish?: boolean;
template?: Template | null;
index?: number;
};
export default async function documentCreator(
ctx: APIContext,
{
title,
text,
icon,
color,
state,
id,
urlId,
publish,
index,
collectionId,
parentDocumentId,
content,
template,
fullWidth,
importId,
apiImportId,
createdAt,
// allows override for import
updatedAt,
editorVersion,
publishedAt,
sourceMetadata,
createdById,
lastModifiedById,
}: Props
): Promise<Document> {
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
const templateId = template ? template.id : undefined;
const eventData = importId || apiImportId ? { source: "import" } : undefined;
if (state && template) {
throw new Error(
"State cannot be set when creating a document from a template"
);
}
if (urlId) {
const existing = await Document.unscoped().findOne({
attributes: ["id"],
transaction,
where: {
urlId,
},
});
if (existing) {
urlId = undefined;
}
}
const titleWithReplacements =
title ??
(template ? TextHelper.replaceTemplateVariables(template.title, user) : "");
const contentWithReplacements = content
? content
: text
? ProsemirrorHelper.toProsemirror(text).toJSON()
: template
? ProsemirrorHelper.replaceTemplateVariables(
await DocumentHelper.toJSON(template),
user
)
: ProsemirrorHelper.toProsemirror("").toJSON();
const document = Document.build({
id,
urlId,
parentDocumentId,
editorVersion,
collectionId,
teamId: user.teamId,
createdAt,
updatedAt: updatedAt ?? createdAt,
lastModifiedById: lastModifiedById ?? createdById ?? user.id,
createdById: createdById ?? user.id,
templateId,
publishedAt,
importId,
apiImportId,
sourceMetadata,
fullWidth: fullWidth ?? template?.fullWidth,
icon: icon ?? template?.icon,
color: color ?? template?.color,
title: titleWithReplacements,
content: contentWithReplacements,
state,
});
document.text = await DocumentHelper.toMarkdown(document, {
includeTitle: false,
});
await document.saveWithCtx(
ctx,
{
silent: !!createdAt,
},
{ data: eventData }
);
if (publish) {
if (!collectionId) {
throw new Error("Collection ID is required to publish");
}
await document.publish(ctx, {
collectionId,
silent: true,
index,
event: !!document.title,
data: eventData,
});
}
// reload to get all of the data needed to present (user, collection etc)
// we need to specify publishedAt to bypass default scope that only returns
// published documents
return Document.findByPk(document.id, {
userId: user.id,
rejectOnEmpty: true,
transaction,
});
}