@terreno/api 0.0.11-beta.1 → 0.0.12

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.
@@ -4,33 +4,44 @@ import axios from "axios";
4
4
  import {APIError} from "../errors";
5
5
  import {logger} from "../logger";
6
6
  // Convenience method to send data to a Slack webhook.
7
+ // If `url` is provided, it will be used directly instead of looking up from environment.
8
+ // DEPRECATED: Looking up webhook URLs from the SLACK_WEBHOOKS environment variable by channel name
9
+ // is deprecated and will be removed in a future version. Please pass the `url` parameter directly.
7
10
  export async function sendToSlack(
8
11
  text: string,
9
12
  {
10
13
  slackChannel,
11
14
  shouldThrow = false,
12
15
  env,
13
- }: {slackChannel?: string; shouldThrow?: boolean; env?: string} = {}
16
+ url,
17
+ }: {slackChannel?: string; shouldThrow?: boolean; env?: string; url?: string} = {}
14
18
  ) {
15
- // since Slack now requires a webhook for each channel, we need to store them in the environment
16
- // as an object, so we can look them up by channel name.
17
- const slackWebhooksString = process.env.SLACK_WEBHOOKS;
18
- if (!slackWebhooksString) {
19
- logger.debug("You must set SLACK_WEBHOOKS in the environment to use sendToSlack.");
20
- return;
21
- }
22
- const slackWebhooks = JSON.parse(slackWebhooksString ?? "{}");
23
-
24
- const channel = slackChannel ?? "default";
25
-
26
- const slackWebhookUrl = slackWebhooks[channel] ?? slackWebhooks.default;
19
+ let slackWebhookUrl = url;
27
20
 
28
21
  if (!slackWebhookUrl) {
29
- Sentry.captureException(
30
- new Error(`No webhook url set in env for ${channel}. Slack message not sent`)
22
+ logger.debug(
23
+ "DEPRECATED: Looking up webhook URLs from SLACK_WEBHOOKS environment variable is deprecated and will be removed in a future version. Please pass the url parameter directly."
31
24
  );
32
- logger.debug(`No webhook url set in env for ${channel}.`);
33
- return;
25
+ // since Slack now requires a webhook for each channel, we need to store them in the environment
26
+ // as an object, so we can look them up by channel name.
27
+ const slackWebhooksString = process.env.SLACK_WEBHOOKS;
28
+ if (!slackWebhooksString) {
29
+ logger.debug("You must set SLACK_WEBHOOKS in the environment to use sendToSlack.");
30
+ return;
31
+ }
32
+ const slackWebhooks = JSON.parse(slackWebhooksString ?? "{}");
33
+
34
+ const channel = slackChannel ?? "default";
35
+
36
+ slackWebhookUrl = slackWebhooks[channel] ?? slackWebhooks.default;
37
+
38
+ if (!slackWebhookUrl) {
39
+ Sentry.captureException(
40
+ new Error(`No webhook url set in env for ${channel}. Slack message not sent`)
41
+ );
42
+ logger.debug(`No webhook url set in env for ${channel}.`);
43
+ return;
44
+ }
34
45
  }
35
46
 
36
47
  let formattedText = text;
@@ -5,7 +5,7 @@ import type TestAgent from "supertest/lib/agent";
5
5
 
6
6
  import {modelRouter} from "./api";
7
7
  import {addAuthRoutes, setupAuth} from "./auth";
8
- import {Permissions} from "./permissions";
8
+ import {OwnerQueryFilter, Permissions} from "./permissions";
9
9
  import {
10
10
  authAsUser,
11
11
  type Food,
@@ -217,3 +217,72 @@ describe("permissions", () => {
217
217
  });
218
218
  });
219
219
  });
220
+
221
+ describe("permissions module", () => {
222
+ describe("OwnerQueryFilter", () => {
223
+ it("returns ownerId filter when user is provided", () => {
224
+ const user = {id: "user-123"} as any;
225
+ const filter = OwnerQueryFilter(user);
226
+ expect(filter).toEqual({ownerId: "user-123"});
227
+ });
228
+
229
+ it("returns null when user is undefined", () => {
230
+ const filter = OwnerQueryFilter(undefined);
231
+ expect(filter).toBeNull();
232
+ });
233
+ });
234
+
235
+ describe("Permissions.IsAuthenticatedOrReadOnly", () => {
236
+ it("returns true for authenticated non-anonymous users", () => {
237
+ const user = {id: "user-123", isAnonymous: false} as any;
238
+ expect(Permissions.IsAuthenticatedOrReadOnly("create", user)).toBe(true);
239
+ });
240
+
241
+ it("returns true for read methods when user is anonymous", () => {
242
+ const user = {id: "user-123", isAnonymous: true} as any;
243
+ expect(Permissions.IsAuthenticatedOrReadOnly("list", user)).toBe(true);
244
+ expect(Permissions.IsAuthenticatedOrReadOnly("read", user)).toBe(true);
245
+ });
246
+
247
+ it("returns false for write methods when user is anonymous", () => {
248
+ const user = {id: "user-123", isAnonymous: true} as any;
249
+ expect(Permissions.IsAuthenticatedOrReadOnly("create", user)).toBe(false);
250
+ expect(Permissions.IsAuthenticatedOrReadOnly("update", user)).toBe(false);
251
+ expect(Permissions.IsAuthenticatedOrReadOnly("delete", user)).toBe(false);
252
+ });
253
+ });
254
+
255
+ describe("Permissions.IsOwnerOrReadOnly", () => {
256
+ it("returns true when no object is provided", () => {
257
+ expect(Permissions.IsOwnerOrReadOnly("update", {id: "user-123"} as any, undefined)).toBe(
258
+ true
259
+ );
260
+ });
261
+
262
+ it("returns true for admin users", () => {
263
+ const user = {admin: true, id: "admin-123"} as any;
264
+ const obj = {ownerId: "other-user"};
265
+ expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(true);
266
+ });
267
+
268
+ it("returns true when user is owner", () => {
269
+ const user = {id: "user-123"} as any;
270
+ const obj = {ownerId: "user-123"};
271
+ expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(true);
272
+ });
273
+
274
+ it("returns true for read methods when not owner", () => {
275
+ const user = {id: "user-123"} as any;
276
+ const obj = {ownerId: "other-user"};
277
+ expect(Permissions.IsOwnerOrReadOnly("list", user, obj)).toBe(true);
278
+ expect(Permissions.IsOwnerOrReadOnly("read", user, obj)).toBe(true);
279
+ });
280
+
281
+ it("returns false for write methods when not owner", () => {
282
+ const user = {id: "user-123"} as any;
283
+ const obj = {ownerId: "other-user"};
284
+ expect(Permissions.IsOwnerOrReadOnly("update", user, obj)).toBe(false);
285
+ expect(Permissions.IsOwnerOrReadOnly("delete", user, obj)).toBe(false);
286
+ });
287
+ });
288
+ });
@@ -63,3 +63,61 @@ describe("populate functions", () => {
63
63
  expect(populated.likesIds[1].userId.name).toBeUndefined();
64
64
  });
65
65
  });
66
+
67
+ describe("unpopulate edge cases", () => {
68
+ it("throws error when path is empty", () => {
69
+ const doc = {name: "test"};
70
+ expect(() => unpopulate(doc as any, "")).toThrow("path is required");
71
+ });
72
+
73
+ it("unpopulates single populated field", () => {
74
+ const doc = {
75
+ name: "test",
76
+ ownerId: {_id: "owner-123", email: "owner@test.com"},
77
+ };
78
+ const result = unpopulate(doc as any, "ownerId") as any;
79
+ expect(result.ownerId).toBe("owner-123");
80
+ });
81
+
82
+ it("unpopulates array of populated fields", () => {
83
+ const doc = {
84
+ items: [{_id: "item-1", name: "Item 1"}, {_id: "item-2", name: "Item 2"}, "item-3"],
85
+ name: "test",
86
+ };
87
+ const result = unpopulate(doc as any, "items") as any;
88
+ expect(result.items).toEqual(["item-1", "item-2", "item-3"]);
89
+ });
90
+
91
+ it("handles nested paths", () => {
92
+ const doc = {
93
+ name: "test",
94
+ nested: {
95
+ items: [
96
+ {_id: "item-1", name: "Item 1"},
97
+ {_id: "item-2", name: "Item 2"},
98
+ ],
99
+ },
100
+ };
101
+ const result = unpopulate(doc as any, "nested.items") as any;
102
+ expect(result.nested.items).toEqual(["item-1", "item-2"]);
103
+ });
104
+
105
+ it("returns original doc when path does not exist", () => {
106
+ const doc = {name: "test"};
107
+ const result = unpopulate(doc as any, "nonexistent") as any;
108
+ expect(result).toEqual(doc);
109
+ });
110
+
111
+ it("handles nested array paths", () => {
112
+ const doc = {
113
+ containers: [
114
+ {items: [{_id: "item-1"}, {_id: "item-2"}]},
115
+ {items: [{_id: "item-3"}, {_id: "item-4"}]},
116
+ ],
117
+ name: "test",
118
+ };
119
+ const result = unpopulate(doc as any, "containers.items") as any;
120
+ expect(result.containers[0].items).toEqual(["item-1", "item-2"]);
121
+ expect(result.containers[1].items).toEqual(["item-3", "item-4"]);
122
+ });
123
+ });
package/src/utils.test.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {describe, expect, it, spyOn} from "bun:test";
2
2
  import mongoose from "mongoose";
3
3
 
4
- import {checkModelsStrict, isValidObjectId} from "./utils";
4
+ import {checkModelsStrict, isValidObjectId, timeout} from "./utils";
5
5
 
6
6
  describe("utils", () => {
7
7
  describe("isValidObjectId", () => {
@@ -191,4 +191,29 @@ describe("utils", () => {
191
191
  }
192
192
  });
193
193
  });
194
+
195
+ describe("timeout", () => {
196
+ it("resolves after specified time", async () => {
197
+ const start = Date.now();
198
+ await timeout(50);
199
+ const elapsed = Date.now() - start;
200
+ expect(elapsed).toBeGreaterThanOrEqual(40);
201
+ });
202
+ });
203
+
204
+ describe("isValidObjectId additional cases", () => {
205
+ it("returns true for valid ObjectId strings", () => {
206
+ expect(isValidObjectId("507f1f77bcf86cd799439011")).toBe(true);
207
+ });
208
+
209
+ it("returns false for invalid ObjectId strings", () => {
210
+ expect(isValidObjectId("invalid-id")).toBe(false);
211
+ expect(isValidObjectId("12345")).toBe(false);
212
+ expect(isValidObjectId("")).toBe(false);
213
+ });
214
+
215
+ it("returns false for 12-character strings that are not valid ObjectIds", () => {
216
+ expect(isValidObjectId("123456789012")).toBe(false);
217
+ });
218
+ });
194
219
  });