|
1 | | -import type { Driver } from "neo4j-driver"; |
| 1 | +import neo4j, { type Driver } from "neo4j-driver"; |
2 | 2 | import { W3IDBuilder } from "w3id"; |
3 | 3 | import { deserializeValue, serializeValue } from "./schema"; |
4 | 4 | import type { |
| 5 | + AppendEnvelopeOperationLogParams, |
5 | 6 | Envelope, |
| 7 | + EnvelopeOperationLogEntry, |
6 | 8 | GetAllEnvelopesResult, |
| 9 | + GetEnvelopeOperationLogsResult, |
7 | 10 | MetaEnvelope, |
8 | 11 | MetaEnvelopeResult, |
9 | 12 | SearchMetaEnvelopesResult, |
@@ -888,6 +891,155 @@ export class DbService { |
888 | 891 | return count; |
889 | 892 | } |
890 | 893 |
|
| 894 | + /** |
| 895 | + * Gets the metaEnvelope id and ontology for an envelope by envelope id. |
| 896 | + * Used when logging updateEnvelopeValue (resolver only has envelopeId). |
| 897 | + */ |
| 898 | + async getMetaEnvelopeIdByEnvelopeId( |
| 899 | + envelopeId: string, |
| 900 | + eName: string, |
| 901 | + ): Promise<{ metaEnvelopeId: string; ontology: string } | null> { |
| 902 | + if (!eName) { |
| 903 | + throw new Error("eName is required"); |
| 904 | + } |
| 905 | + const result = await this.runQueryInternal( |
| 906 | + ` |
| 907 | + MATCH (m:MetaEnvelope { eName: $eName })-[:LINKS_TO]->(e:Envelope { id: $envelopeId }) |
| 908 | + RETURN m.id AS metaEnvelopeId, m.ontology AS ontology |
| 909 | + `, |
| 910 | + { envelopeId, eName }, |
| 911 | + ); |
| 912 | + const record = result.records[0]; |
| 913 | + if (!record) return null; |
| 914 | + return { |
| 915 | + metaEnvelopeId: record.get("metaEnvelopeId"), |
| 916 | + ontology: record.get("ontology"), |
| 917 | + }; |
| 918 | + } |
| 919 | + |
| 920 | + /** |
| 921 | + * Appends an envelope operation log entry (create, update, delete, update_envelope_value). |
| 922 | + */ |
| 923 | + async appendEnvelopeOperationLog( |
| 924 | + params: AppendEnvelopeOperationLogParams, |
| 925 | + ): Promise<void> { |
| 926 | + if (!params.eName) { |
| 927 | + throw new Error("eName is required for envelope operation logs"); |
| 928 | + } |
| 929 | + const logId = (await new W3IDBuilder().build()).id; |
| 930 | + const platformValue = |
| 931 | + params.platform !== null && params.platform !== undefined |
| 932 | + ? params.platform |
| 933 | + : null; |
| 934 | + await this.runQueryInternal( |
| 935 | + ` |
| 936 | + CREATE (l:EnvelopeOperationLog { |
| 937 | + id: $id, |
| 938 | + eName: $eName, |
| 939 | + metaEnvelopeId: $metaEnvelopeId, |
| 940 | + envelopeHash: $envelopeHash, |
| 941 | + operation: $operation, |
| 942 | + platform: $platform, |
| 943 | + timestamp: $timestamp, |
| 944 | + ontology: $ontology |
| 945 | + }) |
| 946 | + `, |
| 947 | + { |
| 948 | + id: logId, |
| 949 | + eName: params.eName, |
| 950 | + metaEnvelopeId: params.metaEnvelopeId, |
| 951 | + envelopeHash: params.envelopeHash, |
| 952 | + operation: params.operation, |
| 953 | + platform: platformValue, |
| 954 | + timestamp: params.timestamp, |
| 955 | + ontology: params.ontology ?? null, |
| 956 | + }, |
| 957 | + ); |
| 958 | + } |
| 959 | + |
| 960 | + /** |
| 961 | + * Returns paginated envelope operation logs for an eName. |
| 962 | + * Ordered by timestamp DESC, then id ASC for stable cursor pagination. |
| 963 | + */ |
| 964 | + async getEnvelopeOperationLogs( |
| 965 | + eName: string, |
| 966 | + options: { limit: number; cursor?: string | null }, |
| 967 | + ): Promise<GetEnvelopeOperationLogsResult> { |
| 968 | + if (!eName) { |
| 969 | + throw new Error("eName is required for getting envelope operation logs"); |
| 970 | + } |
| 971 | + const limit = Math.min(Math.max(1, options.limit || 20), 100); |
| 972 | + const cursor = options.cursor ?? null; |
| 973 | + |
| 974 | + // Fetch limit+1 to know if there's a next page. Cursor format: "timestamp|id" (| avoids colons in ISO timestamp). |
| 975 | + const [cursorTs = "", cursorId = ""] = cursor |
| 976 | + ? cursor.split("|") |
| 977 | + : []; |
| 978 | + const result = await this.runQueryInternal( |
| 979 | + cursor |
| 980 | + ? ` |
| 981 | + MATCH (l:EnvelopeOperationLog { eName: $eName }) |
| 982 | + WHERE (l.timestamp < $cursorTs) OR (l.timestamp = $cursorTs AND l.id > $cursorId) |
| 983 | + WITH l |
| 984 | + ORDER BY l.timestamp DESC, l.id ASC |
| 985 | + LIMIT $limitPlusOne |
| 986 | + RETURN l.id AS id, l.eName AS eName, l.metaEnvelopeId AS metaEnvelopeId, |
| 987 | + l.envelopeHash AS envelopeHash, l.operation AS operation, |
| 988 | + l.platform AS platform, l.timestamp AS timestamp, l.ontology AS ontology |
| 989 | + ` |
| 990 | + : ` |
| 991 | + MATCH (l:EnvelopeOperationLog { eName: $eName }) |
| 992 | + WITH l |
| 993 | + ORDER BY l.timestamp DESC, l.id ASC |
| 994 | + LIMIT $limitPlusOne |
| 995 | + RETURN l.id AS id, l.eName AS eName, l.metaEnvelopeId AS metaEnvelopeId, |
| 996 | + l.envelopeHash AS envelopeHash, l.operation AS operation, |
| 997 | + l.platform AS platform, l.timestamp AS timestamp, l.ontology AS ontology |
| 998 | + `, |
| 999 | + cursor |
| 1000 | + ? { |
| 1001 | + eName, |
| 1002 | + limitPlusOne: neo4j.int(limit + 1), |
| 1003 | + cursorTs, |
| 1004 | + cursorId, |
| 1005 | + } |
| 1006 | + : { eName, limitPlusOne: neo4j.int(limit + 1) }, |
| 1007 | + ); |
| 1008 | + |
| 1009 | + const rows = result.records.map((r) => ({ |
| 1010 | + id: r.get("id"), |
| 1011 | + eName: r.get("eName"), |
| 1012 | + metaEnvelopeId: r.get("metaEnvelopeId"), |
| 1013 | + envelopeHash: r.get("envelopeHash"), |
| 1014 | + operation: r.get("operation"), |
| 1015 | + platform: r.get("platform"), |
| 1016 | + timestamp: r.get("timestamp"), |
| 1017 | + ontology: r.get("ontology"), |
| 1018 | + })); |
| 1019 | + |
| 1020 | + const hasMore = rows.length > limit; |
| 1021 | + const logs = (hasMore ? rows.slice(0, limit) : rows).map( |
| 1022 | + (r): EnvelopeOperationLogEntry => ({ |
| 1023 | + id: r.id, |
| 1024 | + eName: r.eName, |
| 1025 | + metaEnvelopeId: r.metaEnvelopeId, |
| 1026 | + envelopeHash: r.envelopeHash, |
| 1027 | + operation: r.operation, |
| 1028 | + platform: r.platform, |
| 1029 | + timestamp: r.timestamp, |
| 1030 | + ...(r.ontology != null && { ontology: r.ontology }), |
| 1031 | + }), |
| 1032 | + ); |
| 1033 | + |
| 1034 | + const last = logs[logs.length - 1]; |
| 1035 | + const nextCursor = |
| 1036 | + hasMore && last |
| 1037 | + ? `${last.timestamp}|${last.id}` |
| 1038 | + : null; |
| 1039 | + |
| 1040 | + return { logs, nextCursor, hasMore }; |
| 1041 | + } |
| 1042 | + |
891 | 1043 | /** |
892 | 1044 | * Closes the database connection. |
893 | 1045 | */ |
|
0 commit comments