server/models/DocumentInsight.ts
Rama main · réplica de lectura del repositorio original.
import type { InferAttributes, InferCreationAttributes } from "sequelize";
import { QueryTypes } from "sequelize";
import {
BelongsTo,
Column,
DataType,
Default,
ForeignKey,
Table,
} from "sequelize-typescript";
import Document from "./Document";
import Team from "./Team";
import IdModel from "./base/IdModel";
import { SkipChangeset } from "./decorators/Changeset";
export enum DocumentInsightPeriod {
Day = "day",
Week = "week",
}
@Table({
tableName: "document_insights",
modelName: "documentInsight",
indexes: [
{
fields: ["documentId", "date", "period"],
unique: true,
},
{
fields: ["teamId", "date"],
},
],
})
class DocumentInsight extends IdModel<
InferAttributes<DocumentInsight>,
Partial<InferCreationAttributes<DocumentInsight>>
> {
@Column(DataType.DATEONLY)
date: string;
@Default(DocumentInsightPeriod.Day)
@Column(DataType.ENUM(...Object.values(DocumentInsightPeriod)))
@SkipChangeset
period: DocumentInsightPeriod;
@Default(0)
@Column(DataType.INTEGER)
@SkipChangeset
viewCount: number;
@Default(0)
@Column(DataType.INTEGER)
@SkipChangeset
viewerCount: number;
@Default(0)
@Column(DataType.INTEGER)
@SkipChangeset
commentCount: number;
@Default(0)
@Column(DataType.INTEGER)
@SkipChangeset
reactionCount: number;
@Default(0)
@Column(DataType.INTEGER)
@SkipChangeset
revisionCount: number;
@Default(0)
@Column(DataType.INTEGER)
@SkipChangeset
editorCount: number;
// associations
@BelongsTo(() => Document, "documentId")
document: Document;
@ForeignKey(() => Document)
@Column(DataType.UUID)
documentId: string;
@BelongsTo(() => Team, "teamId")
team: Team;
@ForeignKey(() => Team)
@Column(DataType.UUID)
teamId: string;
/**
* Aggregate a time window of source activity (views, comments, reactions,
* revisions) into document_insights rows for documents whose id falls in the
* given UUID range. Upserts on the unique (documentId, date, period) index
* so the operation is idempotent.
*
* @param options.periodStart UTC date string (YYYY-MM-DD) marking the start of the window.
* @param options.intervalDays length of the window in days (1 for daily, 7 for weekly).
* @param options.period the period type to write on each row.
* @param options.startUuid inclusive lower bound of the document id partition.
* @param options.endUuid inclusive upper bound of the document id partition.
* @returns the number of rows upserted.
*/
public static async rollupPeriod({
periodStart,
intervalDays,
period,
startUuid,
endUuid,
}: {
periodStart: string;
intervalDays: number;
period: DocumentInsightPeriod;
startUuid: string;
endUuid: string;
}): Promise<number> {
const [{ upserted }] = await this.sequelize!.query<{ upserted: string }>(
`
WITH partitioned_documents AS (
SELECT id, "teamId"
FROM documents
WHERE "deletedAt" IS NULL
AND id >= :startUuid::uuid
AND id <= :endUuid::uuid
),
view_counts AS (
SELECT
e."documentId",
COUNT(*) AS view_count,
COUNT(DISTINCT e."userId") AS viewer_count
FROM events e
INNER JOIN partitioned_documents pd ON pd.id = e."documentId"
WHERE e.name = 'views.create'
AND e."createdAt" >= :periodStart::timestamp AT TIME ZONE 'UTC'
AND e."createdAt" < (:periodStart::timestamp + (:intervalDays * INTERVAL '1 day')) AT TIME ZONE 'UTC'
GROUP BY e."documentId"
),
comment_counts AS (
SELECT c."documentId", COUNT(*) AS comment_count
FROM comments c
INNER JOIN partitioned_documents pd ON pd.id = c."documentId"
WHERE c."createdAt" >= :periodStart::timestamp AT TIME ZONE 'UTC'
AND c."createdAt" < (:periodStart::timestamp + (:intervalDays * INTERVAL '1 day')) AT TIME ZONE 'UTC'
GROUP BY c."documentId"
),
reaction_counts AS (
SELECT c."documentId", COUNT(rx.id) AS reaction_count
FROM reactions rx
INNER JOIN comments c ON c.id = rx."commentId"
INNER JOIN partitioned_documents pd ON pd.id = c."documentId"
WHERE rx."createdAt" >= :periodStart::timestamp AT TIME ZONE 'UTC'
AND rx."createdAt" < (:periodStart::timestamp + (:intervalDays * INTERVAL '1 day')) AT TIME ZONE 'UTC'
GROUP BY c."documentId"
),
revision_counts AS (
SELECT
r."documentId",
COUNT(*) AS revision_count,
COUNT(DISTINCT r."userId") AS editor_count
FROM revisions r
INNER JOIN partitioned_documents pd ON pd.id = r."documentId"
WHERE r."createdAt" >= :periodStart::timestamp AT TIME ZONE 'UTC'
AND r."createdAt" < (:periodStart::timestamp + (:intervalDays * INTERVAL '1 day')) AT TIME ZONE 'UTC'
GROUP BY r."documentId"
),
active AS (
SELECT "documentId" FROM view_counts
UNION SELECT "documentId" FROM comment_counts
UNION SELECT "documentId" FROM reaction_counts
UNION SELECT "documentId" FROM revision_counts
),
inserted AS (
INSERT INTO document_insights (
id, "documentId", "teamId", date, period,
"viewCount", "viewerCount",
"commentCount", "reactionCount",
"revisionCount", "editorCount",
"createdAt", "updatedAt"
)
SELECT
uuid_generate_v4(),
pd.id,
pd."teamId",
:periodStart::date,
:period,
COALESCE(v.view_count, 0),
COALESCE(v.viewer_count, 0),
COALESCE(c.comment_count, 0),
COALESCE(rx.reaction_count, 0),
COALESCE(r.revision_count, 0),
COALESCE(r.editor_count, 0),
NOW(), NOW()
FROM active a
INNER JOIN partitioned_documents pd ON pd.id = a."documentId"
LEFT JOIN view_counts v ON v."documentId" = pd.id
LEFT JOIN comment_counts c ON c."documentId" = pd.id
LEFT JOIN reaction_counts rx ON rx."documentId" = pd.id
LEFT JOIN revision_counts r ON r."documentId" = pd.id
ON CONFLICT ("documentId", date, period) DO UPDATE SET
"viewCount" = EXCLUDED."viewCount",
"viewerCount" = EXCLUDED."viewerCount",
"commentCount" = EXCLUDED."commentCount",
"reactionCount" = EXCLUDED."reactionCount",
"revisionCount" = EXCLUDED."revisionCount",
"editorCount" = EXCLUDED."editorCount",
"updatedAt" = NOW()
RETURNING 1
)
SELECT COUNT(*)::text AS upserted FROM inserted
`,
{
replacements: {
periodStart,
intervalDays,
period,
startUuid,
endUuid,
},
type: QueryTypes.SELECT,
}
);
return parseInt(upserted, 10);
}
}
export default DocumentInsight;