liwi-mongo 13.1.0 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -3
- package/dist/definitions/MongoConnection.d.ts.map +1 -1
- package/dist/definitions/MongoCursor.d.ts.map +1 -1
- package/dist/definitions/MongoQueryCollection.d.ts.map +1 -1
- package/dist/definitions/MongoQuerySingleItem.d.ts.map +1 -1
- package/dist/definitions/MongoRegistry.d.ts +20 -0
- package/dist/definitions/MongoRegistry.d.ts.map +1 -0
- package/dist/definitions/MongoRegistry.test.d.ts +2 -0
- package/dist/definitions/MongoRegistry.test.d.ts.map +1 -0
- package/dist/definitions/MongoRegistryStore.d.ts +7 -0
- package/dist/definitions/MongoRegistryStore.d.ts.map +1 -0
- package/dist/definitions/MongoStore.d.ts +9 -1
- package/dist/definitions/MongoStore.d.ts.map +1 -1
- package/dist/definitions/MongoStoreIndexes.test.d.ts +2 -0
- package/dist/definitions/MongoStoreIndexes.test.d.ts.map +1 -0
- package/dist/definitions/index.d.ts +8 -1
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/indexes/applyIndexPlan.d.ts +11 -0
- package/dist/definitions/indexes/applyIndexPlan.d.ts.map +1 -0
- package/dist/definitions/indexes/buildIndexName.d.ts +3 -0
- package/dist/definitions/indexes/buildIndexName.d.ts.map +1 -0
- package/dist/definitions/indexes/buildIndexName.test.d.ts +2 -0
- package/dist/definitions/indexes/buildIndexName.test.d.ts.map +1 -0
- package/dist/definitions/indexes/deepEqualUnordered.d.ts +2 -0
- package/dist/definitions/indexes/deepEqualUnordered.d.ts.map +1 -0
- package/dist/definitions/indexes/diffIndexes.d.ts +13 -0
- package/dist/definitions/indexes/diffIndexes.d.ts.map +1 -0
- package/dist/definitions/indexes/diffIndexes.test.d.ts +2 -0
- package/dist/definitions/indexes/diffIndexes.test.d.ts.map +1 -0
- package/dist/definitions/indexes/formatIndexPlan.d.ts +5 -0
- package/dist/definitions/indexes/formatIndexPlan.d.ts.map +1 -0
- package/dist/definitions/indexes/formatIndexPlan.test.d.ts +2 -0
- package/dist/definitions/indexes/formatIndexPlan.test.d.ts.map +1 -0
- package/dist/definitions/indexes/isIndexConflictError.d.ts +4 -0
- package/dist/definitions/indexes/isIndexConflictError.d.ts.map +1 -0
- package/dist/definitions/indexes/normalizeIndex.d.ts +34 -0
- package/dist/definitions/indexes/normalizeIndex.d.ts.map +1 -0
- package/dist/definitions/indexes/normalizeIndex.test.d.ts +2 -0
- package/dist/definitions/indexes/normalizeIndex.test.d.ts.map +1 -0
- package/dist/definitions/indexes/runIndexesCli.d.ts +11 -0
- package/dist/definitions/indexes/runIndexesCli.d.ts.map +1 -0
- package/dist/definitions/indexes/runIndexesCli.test.d.ts +2 -0
- package/dist/definitions/indexes/runIndexesCli.test.d.ts.map +1 -0
- package/dist/definitions/indexes/syncIndexes.integration.test.d.ts +2 -0
- package/dist/definitions/indexes/syncIndexes.integration.test.d.ts.map +1 -0
- package/dist/definitions/indexes/types.d.ts +72 -0
- package/dist/definitions/indexes/types.d.ts.map +1 -0
- package/dist/index-node.mjs +626 -10
- package/dist/index-node.mjs.map +1 -1
- package/package.json +10 -7
- package/src/MongoRegistry.test.ts +136 -0
- package/src/MongoRegistry.ts +108 -0
- package/src/MongoRegistryStore.ts +11 -0
- package/src/MongoStore.ts +63 -1
- package/src/MongoStoreIndexes.test.ts +259 -0
- package/src/index.ts +26 -1
- package/src/indexes/applyIndexPlan.ts +149 -0
- package/src/indexes/buildIndexName.test.ts +34 -0
- package/src/indexes/buildIndexName.ts +8 -0
- package/src/indexes/deepEqualUnordered.ts +36 -0
- package/src/indexes/diffIndexes.test.ts +290 -0
- package/src/indexes/diffIndexes.ts +234 -0
- package/src/indexes/formatIndexPlan.test.ts +105 -0
- package/src/indexes/formatIndexPlan.ts +71 -0
- package/src/indexes/isIndexConflictError.ts +20 -0
- package/src/indexes/normalizeIndex.test.ts +154 -0
- package/src/indexes/normalizeIndex.ts +191 -0
- package/src/indexes/runIndexesCli.test.ts +119 -0
- package/src/indexes/runIndexesCli.ts +74 -0
- package/src/indexes/syncIndexes.integration.test.ts +182 -0
- package/src/indexes/types.ts +115 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, before, describe, it } from "node:test";
|
|
3
|
+
import type { IndexDescriptionInfo } from "mongodb";
|
|
4
|
+
import type { MongoMemoryServer } from "mongodb-memory-server-core";
|
|
5
|
+
import type { MongoBaseModel } from "../MongoBaseModel.ts";
|
|
6
|
+
import MongoConnection from "../MongoConnection.ts";
|
|
7
|
+
import MongoStore from "../MongoStore.ts";
|
|
8
|
+
import { isMongoIndexPlanEmpty } from "./diffIndexes.ts";
|
|
9
|
+
import { formatIndexPlan } from "./formatIndexPlan.ts";
|
|
10
|
+
import type { MongoIndex } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
interface Task extends MongoBaseModel {
|
|
13
|
+
completed: boolean;
|
|
14
|
+
label: string;
|
|
15
|
+
status: string;
|
|
16
|
+
tenant: string;
|
|
17
|
+
sessionEnd: Date;
|
|
18
|
+
metadata: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const declaredIndexes: MongoIndex<Task>[] = [
|
|
22
|
+
{ key: { completed: 1, created: 1 } },
|
|
23
|
+
{ key: { label: "text" } },
|
|
24
|
+
{ key: { status: 1 }, collation: { locale: "en" } },
|
|
25
|
+
{ key: { sessionEnd: 1 }, expireAfterSeconds: 3600 },
|
|
26
|
+
{ key: { tenant: 1 }, partialFilterExpression: { completed: true } },
|
|
27
|
+
{ key: { "metadata.$**": 1 } },
|
|
28
|
+
{ key: { created: -1 }, name: "created_desc", sparse: true },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const declaredIndexNames = [
|
|
32
|
+
"completed_1_created_1",
|
|
33
|
+
"created_desc",
|
|
34
|
+
"label_text",
|
|
35
|
+
"metadata.$**_1",
|
|
36
|
+
"sessionEnd_1",
|
|
37
|
+
"status_1",
|
|
38
|
+
"tenant_1",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
describe(
|
|
42
|
+
"syncIndexes (integration)",
|
|
43
|
+
{
|
|
44
|
+
skip: !process.env.LIWI_MONGO_INTEGRATION && "set LIWI_MONGO_INTEGRATION=1",
|
|
45
|
+
timeout: 120_000,
|
|
46
|
+
},
|
|
47
|
+
() => {
|
|
48
|
+
let mongod: MongoMemoryServer;
|
|
49
|
+
let connection: MongoConnection;
|
|
50
|
+
|
|
51
|
+
before(async () => {
|
|
52
|
+
const { MongoMemoryServer: MemoryServer } =
|
|
53
|
+
await import("mongodb-memory-server-core");
|
|
54
|
+
mongod = await MemoryServer.create();
|
|
55
|
+
connection = new MongoConnection({
|
|
56
|
+
host: "127.0.0.1",
|
|
57
|
+
port: mongod.instanceInfo!.port,
|
|
58
|
+
database: "liwi-mongo-indexes-test",
|
|
59
|
+
});
|
|
60
|
+
await connection.getConnection();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
after(async () => {
|
|
64
|
+
await connection.close();
|
|
65
|
+
await mongod.stop();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const createStore = (indexes: MongoIndex<Task>[]): MongoStore<Task> =>
|
|
69
|
+
new MongoStore<Task>(connection, "tasks", { indexes });
|
|
70
|
+
|
|
71
|
+
const listIndexes = async (
|
|
72
|
+
store: MongoStore<Task>,
|
|
73
|
+
): Promise<IndexDescriptionInfo[]> => {
|
|
74
|
+
const collection = await store.collection;
|
|
75
|
+
return collection.listIndexes().toArray();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const listIndexNames = async (
|
|
79
|
+
store: MongoStore<Task>,
|
|
80
|
+
): Promise<string[]> => {
|
|
81
|
+
const indexes = await listIndexes(store);
|
|
82
|
+
return indexes
|
|
83
|
+
.map(({ name }) => name)
|
|
84
|
+
.filter((name) => name !== undefined)
|
|
85
|
+
.toSorted();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
it("creates every declared index with the name mongo derives", async () => {
|
|
89
|
+
const store = createStore(declaredIndexes);
|
|
90
|
+
|
|
91
|
+
const result = await store.syncIndexes();
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(result.created.toSorted(), declaredIndexNames);
|
|
94
|
+
assert.deepEqual(await listIndexNames(store), [
|
|
95
|
+
"_id_",
|
|
96
|
+
...declaredIndexNames,
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("plans nothing on a second run, for every index type", async () => {
|
|
101
|
+
const store = createStore(declaredIndexes);
|
|
102
|
+
|
|
103
|
+
const plan = await store.planIndexes();
|
|
104
|
+
|
|
105
|
+
assert.equal(
|
|
106
|
+
isMongoIndexPlanEmpty(plan),
|
|
107
|
+
true,
|
|
108
|
+
`expected an empty plan, got:\n${formatIndexPlan(plan)}`,
|
|
109
|
+
);
|
|
110
|
+
assert.deepEqual(plan.unchanged.toSorted(), declaredIndexNames);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("applies a ttl change in place with collMod", async () => {
|
|
114
|
+
const store = createStore(
|
|
115
|
+
declaredIndexes.map((index) =>
|
|
116
|
+
index.expireAfterSeconds
|
|
117
|
+
? { ...index, expireAfterSeconds: 60 }
|
|
118
|
+
: index,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const result = await store.syncIndexes();
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(result.modified, ["sessionEnd_1"]);
|
|
125
|
+
assert.deepEqual(result.created, []);
|
|
126
|
+
assert.deepEqual(result.dropped, []);
|
|
127
|
+
|
|
128
|
+
const indexes = await listIndexes(store);
|
|
129
|
+
assert.equal(
|
|
130
|
+
indexes.find(({ name }) => name === "sessionEnd_1")?.expireAfterSeconds,
|
|
131
|
+
60,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("recreates an index whose key changed", async () => {
|
|
136
|
+
const store = createStore([
|
|
137
|
+
...declaredIndexes.filter(({ name }) => name !== "created_desc"),
|
|
138
|
+
{ key: { created: 1 }, name: "created_desc", sparse: true },
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const result = await store.syncIndexes();
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(result.created, ["created_desc"]);
|
|
144
|
+
assert.deepEqual(result.dropped, ["created_desc"]);
|
|
145
|
+
|
|
146
|
+
const indexes = await listIndexes(store);
|
|
147
|
+
assert.deepEqual(
|
|
148
|
+
indexes.find(({ name }) => name === "created_desc")?.key,
|
|
149
|
+
{ created: 1 },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("drops undeclared indexes and keeps _id_", async () => {
|
|
154
|
+
const store = createStore([{ key: { completed: 1, created: 1 } }]);
|
|
155
|
+
|
|
156
|
+
const result = await store.syncIndexes();
|
|
157
|
+
|
|
158
|
+
assert.equal(result.dropped.includes("_id_"), false);
|
|
159
|
+
assert.deepEqual(
|
|
160
|
+
result.dropped.toSorted(),
|
|
161
|
+
declaredIndexNames.filter((name) => name !== "completed_1_created_1"),
|
|
162
|
+
);
|
|
163
|
+
assert.deepEqual(await listIndexNames(store), [
|
|
164
|
+
"_id_",
|
|
165
|
+
"completed_1_created_1",
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("keeps undeclared indexes when dropping is off", async () => {
|
|
170
|
+
const store = createStore([]);
|
|
171
|
+
|
|
172
|
+
const plan = await store.planIndexes({ dropUndeclaredIndexes: false });
|
|
173
|
+
await store.syncIndexes({ dropUndeclaredIndexes: false });
|
|
174
|
+
|
|
175
|
+
assert.deepEqual(plan.undeclaredKept, ["completed_1_created_1"]);
|
|
176
|
+
assert.deepEqual(await listIndexNames(store), [
|
|
177
|
+
"_id_",
|
|
178
|
+
"completed_1_created_1",
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Criteria, DottedPaths } from "liwi-store";
|
|
2
|
+
import type {
|
|
3
|
+
CollationOptions,
|
|
4
|
+
IndexDescription,
|
|
5
|
+
IndexDescriptionInfo,
|
|
6
|
+
} from "mongodb";
|
|
7
|
+
import type { MongoBaseModel } from "../MongoBaseModel.ts";
|
|
8
|
+
|
|
9
|
+
export type MongoIndexDirection =
|
|
10
|
+
| -1
|
|
11
|
+
| "2d"
|
|
12
|
+
| "2dsphere"
|
|
13
|
+
| "hashed"
|
|
14
|
+
| "text"
|
|
15
|
+
| 1;
|
|
16
|
+
|
|
17
|
+
export type MongoIndexKey<Model extends MongoBaseModel<any>> = Partial<
|
|
18
|
+
Record<
|
|
19
|
+
DottedPaths<Model> | "$**" | `${DottedPaths<Model>}.$**`,
|
|
20
|
+
MongoIndexDirection
|
|
21
|
+
>
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export interface MongoIndexOptions<Model extends MongoBaseModel<any>> {
|
|
25
|
+
name?: string;
|
|
26
|
+
unique?: boolean;
|
|
27
|
+
sparse?: boolean;
|
|
28
|
+
hidden?: boolean;
|
|
29
|
+
expireAfterSeconds?: number;
|
|
30
|
+
partialFilterExpression?: Criteria<Model>;
|
|
31
|
+
collation?: CollationOptions;
|
|
32
|
+
weights?: Partial<Record<DottedPaths<Model>, number>>;
|
|
33
|
+
|
|
34
|
+
default_language?: string;
|
|
35
|
+
|
|
36
|
+
language_override?: string;
|
|
37
|
+
wildcardProjection?: Record<string, 0 | 1>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MongoIndex<
|
|
41
|
+
Model extends MongoBaseModel<any>,
|
|
42
|
+
> extends MongoIndexOptions<Model> {
|
|
43
|
+
key: MongoIndexKey<Model>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SyncIndexesOptions {
|
|
47
|
+
dryRun?: boolean;
|
|
48
|
+
dropUndeclaredIndexes?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type MongoIndexComparedField =
|
|
52
|
+
| "collation"
|
|
53
|
+
| "default_language"
|
|
54
|
+
| "expireAfterSeconds"
|
|
55
|
+
| "hidden"
|
|
56
|
+
| "key"
|
|
57
|
+
| "language_override"
|
|
58
|
+
| "partialFilterExpression"
|
|
59
|
+
| "sparse"
|
|
60
|
+
| "unique"
|
|
61
|
+
| "weights"
|
|
62
|
+
| "wildcardProjection";
|
|
63
|
+
|
|
64
|
+
export interface MongoIndexDifference {
|
|
65
|
+
field: MongoIndexComparedField;
|
|
66
|
+
declared: unknown;
|
|
67
|
+
existing: unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MongoIndexCollModChanges {
|
|
71
|
+
expireAfterSeconds?: number;
|
|
72
|
+
hidden?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MongoIndexPlanCreate {
|
|
76
|
+
name: string;
|
|
77
|
+
index: IndexDescription;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface MongoIndexPlanRecreate {
|
|
81
|
+
name: string;
|
|
82
|
+
index: IndexDescription;
|
|
83
|
+
existing: IndexDescriptionInfo;
|
|
84
|
+
differences: MongoIndexDifference[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MongoIndexPlanCollMod {
|
|
88
|
+
name: string;
|
|
89
|
+
changes: MongoIndexCollModChanges;
|
|
90
|
+
differences: MongoIndexDifference[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface MongoIndexPlanDrop {
|
|
94
|
+
name: string;
|
|
95
|
+
existing: IndexDescriptionInfo;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MongoIndexPlan {
|
|
99
|
+
collectionName: string;
|
|
100
|
+
toCreate: MongoIndexPlanCreate[];
|
|
101
|
+
toRecreate: MongoIndexPlanRecreate[];
|
|
102
|
+
toCollMod: MongoIndexPlanCollMod[];
|
|
103
|
+
toDrop: MongoIndexPlanDrop[];
|
|
104
|
+
unchanged: string[];
|
|
105
|
+
undeclaredKept: string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface MongoIndexSyncResult {
|
|
109
|
+
collectionName: string;
|
|
110
|
+
plan: MongoIndexPlan;
|
|
111
|
+
dryRun: boolean;
|
|
112
|
+
created: string[];
|
|
113
|
+
dropped: string[];
|
|
114
|
+
modified: string[];
|
|
115
|
+
}
|