mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
0b213bd6b8
* 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>
165 lines
3.6 KiB
TypeScript
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,
|
|
});
|
|
}
|