@tellescope/sdk 1.244.1 → 1.244.2

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.
Files changed (35) hide show
  1. package/lib/cjs/sdk.d.ts +3 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js.map +1 -1
  4. package/lib/cjs/tests/api_tests/elation_user_id.test.d.ts +6 -0
  5. package/lib/cjs/tests/api_tests/elation_user_id.test.d.ts.map +1 -0
  6. package/lib/cjs/tests/api_tests/elation_user_id.test.js +106 -0
  7. package/lib/cjs/tests/api_tests/elation_user_id.test.js.map +1 -0
  8. package/lib/cjs/tests/api_tests/get_some_projection.test.d.ts +6 -0
  9. package/lib/cjs/tests/api_tests/get_some_projection.test.d.ts.map +1 -0
  10. package/lib/cjs/tests/api_tests/get_some_projection.test.js +373 -0
  11. package/lib/cjs/tests/api_tests/get_some_projection.test.js.map +1 -0
  12. package/lib/cjs/tests/tests.d.ts.map +1 -1
  13. package/lib/cjs/tests/tests.js +169 -133
  14. package/lib/cjs/tests/tests.js.map +1 -1
  15. package/lib/esm/sdk.d.ts +5 -2
  16. package/lib/esm/sdk.d.ts.map +1 -1
  17. package/lib/esm/sdk.js.map +1 -1
  18. package/lib/esm/tests/api_tests/elation_user_id.test.d.ts +6 -0
  19. package/lib/esm/tests/api_tests/elation_user_id.test.d.ts.map +1 -0
  20. package/lib/esm/tests/api_tests/elation_user_id.test.js +102 -0
  21. package/lib/esm/tests/api_tests/elation_user_id.test.js.map +1 -0
  22. package/lib/esm/tests/api_tests/get_some_projection.test.d.ts +6 -0
  23. package/lib/esm/tests/api_tests/get_some_projection.test.d.ts.map +1 -0
  24. package/lib/esm/tests/api_tests/get_some_projection.test.js +369 -0
  25. package/lib/esm/tests/api_tests/get_some_projection.test.js.map +1 -0
  26. package/lib/esm/tests/tests.d.ts.map +1 -1
  27. package/lib/esm/tests/tests.js +169 -133
  28. package/lib/esm/tests/tests.js.map +1 -1
  29. package/lib/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +10 -10
  31. package/src/sdk.ts +5 -4
  32. package/src/tests/api_tests/elation_user_id.test.ts +59 -0
  33. package/src/tests/api_tests/get_some_projection.test.ts +245 -0
  34. package/src/tests/tests.ts +71 -8
  35. package/test_generated.pdf +0 -0
@@ -0,0 +1,245 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ assert,
6
+ async_test,
7
+ log_header,
8
+ } from "@tellescope/testing"
9
+ import { setup_tests } from "../setup"
10
+
11
+ const host = process.env.API_URL || 'http://localhost:8080' as const
12
+
13
+ // Main test function that can be called independently
14
+ export const get_some_projection_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
15
+ log_header("getSome Projection Support")
16
+
17
+ // Create test enduser
18
+ const testEnduser = await sdk.api.endusers.createOne({ fname: 'ProjectionTest', lname: 'User', email: 'projection-test@tellescope.com' })
19
+
20
+ let observationIds: string[] = []
21
+
22
+ try {
23
+ // Create test observations
24
+ const obs1 = await sdk.api.enduser_observations.createOne({
25
+ enduserId: testEnduser.id,
26
+ status: 'final',
27
+ category: 'vital-signs',
28
+ measurement: { value: 120, unit: 'mmHg' },
29
+ })
30
+ const obs2 = await sdk.api.enduser_observations.createOne({
31
+ enduserId: testEnduser.id,
32
+ status: 'final',
33
+ category: 'vital-signs',
34
+ measurement: { value: 80, unit: 'mmHg' },
35
+ })
36
+ const obs3 = await sdk.api.enduser_observations.createOne({
37
+ enduserId: testEnduser.id,
38
+ status: 'final',
39
+ category: 'vital-signs',
40
+ measurement: { value: 98, unit: 'bpm' },
41
+ })
42
+ observationIds = [obs1.id, obs2.id, obs3.id]
43
+
44
+ // Test 1: Inclusion projection returns only specified fields
45
+ await async_test(
46
+ 'projection-inclusion',
47
+ async () => {
48
+ const results = await sdk.api.enduser_observations.getSome({
49
+ filter: { enduserId: testEnduser.id },
50
+ projection: { timestamp: 1, measurement: 1, enduserId: 1 },
51
+ })
52
+ assert(results.length === 3, 'Expected 3 observations with projection', 'Got correct count with projection')
53
+
54
+ for (const r of results) {
55
+ assert(r.id !== undefined, 'id should always be present')
56
+ assert(r.createdAt !== undefined, 'createdAt should always be present')
57
+ assert(r.measurement !== undefined, 'measurement should be present in projection')
58
+ assert(r.enduserId !== undefined, 'enduserId should be present in projection')
59
+ assert(r.timestamp !== undefined, 'timestamp should be present in projection')
60
+ // Fields NOT in projection should be excluded
61
+ assert((r as any).status === undefined, 'status should NOT be present when not in projection')
62
+ assert((r as any).category === undefined, 'category should NOT be present when not in projection')
63
+ }
64
+ return results
65
+ },
66
+ { onResult: () => true },
67
+ )
68
+
69
+ // Test 2: No projection returns all fields (backward compatibility)
70
+ await async_test(
71
+ 'no-projection-returns-all-fields',
72
+ async () => {
73
+ const results = await sdk.api.enduser_observations.getSome({
74
+ filter: { enduserId: testEnduser.id },
75
+ })
76
+ assert(results.length === 3, 'Expected 3 observations without projection')
77
+
78
+ for (const r of results) {
79
+ assert(r.id !== undefined, 'id should be present')
80
+ assert(r.status !== undefined, 'status should be present without projection')
81
+ assert(r.category !== undefined, 'category should be present without projection')
82
+ assert(r.measurement !== undefined, 'measurement should be present without projection')
83
+ assert(r.enduserId !== undefined, 'enduserId should be present without projection')
84
+ }
85
+ return results
86
+ },
87
+ { onResult: () => true },
88
+ )
89
+
90
+ // Test 3: Projection with other filters (filter, limit, sortBy)
91
+ await async_test(
92
+ 'projection-with-filters',
93
+ async () => {
94
+ const results = await sdk.api.enduser_observations.getSome({
95
+ filter: { enduserId: testEnduser.id },
96
+ projection: { measurement: 1 },
97
+ limit: 2,
98
+ sortBy: 'timestamp',
99
+ })
100
+ assert(results.length === 2, 'Expected 2 observations with limit=2', `Got ${results.length}`)
101
+
102
+ for (const r of results) {
103
+ assert(r.measurement !== undefined, 'measurement should be present in projection')
104
+ assert((r as any).status === undefined, 'status should NOT be present when not in projection')
105
+ }
106
+ return results
107
+ },
108
+ { onResult: () => true },
109
+ )
110
+
111
+ // Test 4: Projection works across different models (endusers)
112
+ await async_test(
113
+ 'projection-on-endusers',
114
+ async () => {
115
+ const results = await sdk.api.endusers.getSome({
116
+ filter: { email: 'projection-test@tellescope.com' },
117
+ projection: { fname: 1, email: 1 },
118
+ })
119
+ assert(results.length >= 1, 'Expected at least 1 enduser')
120
+
121
+ const enduser = results[0]
122
+ assert(enduser.id !== undefined, 'id should always be present')
123
+ assert(enduser.fname !== undefined, 'fname should be present in projection')
124
+ assert(enduser.email !== undefined, 'email should be present in projection')
125
+ assert((enduser as any).lname === undefined, 'lname should NOT be present when not in projection')
126
+ return results
127
+ },
128
+ { onResult: () => true },
129
+ )
130
+
131
+ // Test 5: Projection with pagination (lastId)
132
+ await async_test(
133
+ 'projection-with-pagination',
134
+ async () => {
135
+ // First page
136
+ const page1 = await sdk.api.enduser_observations.getSome({
137
+ filter: { enduserId: testEnduser.id },
138
+ projection: { measurement: 1, enduserId: 1 },
139
+ limit: 2,
140
+ sort: 'oldFirst',
141
+ })
142
+ assert(page1.length === 2, 'Expected 2 observations on first page')
143
+
144
+ // Second page using lastId from first page
145
+ const lastId = page1[page1.length - 1].id
146
+ const page2 = await sdk.api.enduser_observations.getSome({
147
+ filter: { enduserId: testEnduser.id },
148
+ projection: { measurement: 1, enduserId: 1 },
149
+ limit: 2,
150
+ sort: 'oldFirst',
151
+ lastId,
152
+ })
153
+ assert(page2.length === 1, 'Expected 1 observation on second page')
154
+
155
+ // Verify consistent fields across pages
156
+ for (const r of [...page1, ...page2]) {
157
+ assert(r.measurement !== undefined, 'measurement should be present across pages')
158
+ assert(r.enduserId !== undefined, 'enduserId should be present across pages')
159
+ assert((r as any).status === undefined, 'status should NOT be present across pages')
160
+ }
161
+ return [...page1, ...page2]
162
+ },
163
+ { onResult: () => true },
164
+ )
165
+
166
+ // Test 6: Empty projection returns all fields (no crash)
167
+ await async_test(
168
+ 'empty-projection',
169
+ async () => {
170
+ const results = await sdk.api.enduser_observations.getSome({
171
+ filter: { enduserId: testEnduser.id },
172
+ projection: {},
173
+ })
174
+ assert(results.length === 3, 'Expected 3 observations with empty projection')
175
+
176
+ for (const r of results) {
177
+ assert(r.status !== undefined, 'status should be present with empty projection')
178
+ assert(r.category !== undefined, 'category should be present with empty projection')
179
+ assert(r.measurement !== undefined, 'measurement should be present with empty projection')
180
+ }
181
+ return results
182
+ },
183
+ { onResult: () => true },
184
+ )
185
+
186
+ // Test 7: Non-admin access with projection (RBA still applies)
187
+ await async_test(
188
+ 'non-admin-projection',
189
+ async () => {
190
+ const results = await sdkNonAdmin.api.enduser_observations.getSome({
191
+ filter: { enduserId: testEnduser.id },
192
+ projection: { measurement: 1, enduserId: 1 },
193
+ })
194
+ // Non-admin should still get results (RBA should apply)
195
+ assert(Array.isArray(results), 'Non-admin should receive an array response')
196
+
197
+ for (const r of results) {
198
+ assert(r.measurement !== undefined, 'measurement should be present for non-admin projection')
199
+ assert(r.enduserId !== undefined, 'enduserId should be present for non-admin projection')
200
+ }
201
+ return results
202
+ },
203
+ { onResult: () => true },
204
+ )
205
+
206
+ } finally {
207
+ // Cleanup: Delete test resources
208
+ try {
209
+ for (const obsId of observationIds) {
210
+ await sdk.api.enduser_observations.deleteOne(obsId)
211
+ }
212
+ await sdk.api.endusers.deleteOne(testEnduser.id)
213
+ } catch (error) {
214
+ console.error('Cleanup error:', error)
215
+ // Try to at least delete the enduser (cascade deletes observations)
216
+ try {
217
+ await sdk.api.endusers.deleteOne(testEnduser.id)
218
+ } catch (deleteError) {
219
+ console.error('Failed to delete test enduser:', deleteError)
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ // Allow running this test file independently
226
+ if (require.main === module) {
227
+ console.log(`🌐 Using API URL: ${host}`)
228
+ const sdk = new Session({ host })
229
+ const sdkNonAdmin = new Session({ host })
230
+
231
+ const runTests = async () => {
232
+ await setup_tests(sdk, sdkNonAdmin)
233
+ await get_some_projection_tests({ sdk, sdkNonAdmin })
234
+ }
235
+
236
+ runTests()
237
+ .then(() => {
238
+ console.log("✅ getSome projection test suite completed successfully")
239
+ process.exit(0)
240
+ })
241
+ .catch((error) => {
242
+ console.error("❌ getSome projection test suite failed:", error)
243
+ process.exit(1)
244
+ })
245
+ }
@@ -36,6 +36,7 @@ import {
36
36
 
37
37
  import { Session, APIQuery, EnduserSession } from "../sdk"
38
38
  import { enduser_observations_acknowledge_tests } from "./api_tests/enduser_observations_acknowledge.test"
39
+ import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
39
40
  import { create_user_notifications_trigger_tests } from "./api_tests/create_user_notifications_trigger.test"
40
41
  import { inbox_thread_assignment_updates_tests } from "./api_tests/inbox_thread_assignment_updates.test"
41
42
  import { inbox_thread_draft_scheduled_tests } from "./api_tests/inbox_thread_draft_scheduled.test"
@@ -85,6 +86,7 @@ import { database_cascade_delete_tests } from "./api_tests/database_cascade_dele
85
86
  import { ai_conversations_tests } from "./api_tests/ai_conversations.test";
86
87
  import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
87
88
  import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
89
+ import { elation_user_id_tests } from "./api_tests/elation_user_id.test";
88
90
 
89
91
  const UniquenessViolationMessage = 'Uniqueness Violation'
90
92
 
@@ -5572,28 +5574,87 @@ const redaction_tests = async () => {
5572
5574
  log_header("Redaction")
5573
5575
 
5574
5576
  const enduser = await sdk.api.endusers.createOne({ email })
5575
- const enduserOther = await sdk.api.endusers.createOne({ email: 'otherenduser@tellescope.com' })
5577
+ const enduserOther = await sdk.api.endusers.createOne({
5578
+ email: 'otherenduser@tellescope.com',
5579
+ unredactedFields: [{ field: 'testField', value: 'testValue' }] as any,
5580
+ })
5576
5581
  await sdk.api.endusers.set_password({ id: enduser.id, password }).catch(console.error)
5577
- await enduserSDK.authenticate(email, password).catch(console.error)
5582
+ await enduserSDK.authenticate(email, password).catch(console.error)
5578
5583
 
5579
5584
  const endusers = await enduserSDK.api.endusers.getSome()
5580
5585
  const forUser = await sdk.api.endusers.getSome()
5581
5586
  assert(endusers.length > 0, "enduser can't fetch others", "enduser get others successful")
5582
5587
 
5588
+ // endusers reading OTHER endusers should only see id, displayName, unredactedFields, and unredactedTags
5589
+ const ALLOWED_OTHER_ENDUSER_FIELDS = ['id', 'displayName', 'unredactedFields', 'unredactedTags']
5590
+ const otherEndusers = endusers.filter(e => e.id !== enduser.id)
5591
+ assert(otherEndusers.length > 0, 'no other endusers found', 'found other endusers to check redaction')
5592
+ for (const other of otherEndusers) {
5593
+ const keys = Object.keys(other)
5594
+ assert(
5595
+ keys.every(k => ALLOWED_OTHER_ENDUSER_FIELDS.includes(k)),
5596
+ `other enduser has extra fields: ${keys.filter(k => !ALLOWED_OTHER_ENDUSER_FIELDS.includes(k)).join(', ')}`,
5597
+ 'other enduser correctly redacted to only id, displayName, unredactedFields, unredactedTags',
5598
+ )
5599
+ assert(
5600
+ keys.includes('id'),
5601
+ `other enduser missing id`,
5602
+ 'other enduser has id',
5603
+ )
5604
+ }
5605
+
5606
+ // verify unredactedFields are preserved for other endusers when defined
5607
+ const otherWithUnredacted = otherEndusers.find(e => e.id === enduserOther.id)
5608
+ if (otherWithUnredacted) {
5609
+ assert(
5610
+ !!(otherWithUnredacted as any).unredactedFields?.length,
5611
+ 'unredactedFields missing on other enduser',
5612
+ 'unredactedFields preserved on other enduser when defined',
5613
+ )
5614
+ }
5615
+
5616
+ // verify unredactedFields/unredactedTags are omitted when not set on the enduser
5617
+ const otherWithoutUnredacted = otherEndusers.find(e => e.id !== enduserOther.id)
5618
+ if (otherWithoutUnredacted) {
5619
+ assert(
5620
+ !('unredactedFields' in otherWithoutUnredacted),
5621
+ 'unredactedFields present when not set',
5622
+ 'unredactedFields omitted when not defined on enduser',
5623
+ )
5624
+ assert(
5625
+ !('unredactedTags' in otherWithoutUnredacted),
5626
+ 'unredactedTags present when not set',
5627
+ 'unredactedTags omitted when not defined on enduser',
5628
+ )
5629
+ }
5630
+
5631
+ // enduser reading their own profile should still have non-redacted fields (not just id + displayName)
5632
+ const selfEnduser = endusers.find(e => e.id === enduser.id)
5633
+ if (selfEnduser) {
5634
+ assert(
5635
+ Object.keys(selfEnduser).length > ALLOWED_OTHER_ENDUSER_FIELDS.length,
5636
+ 'self enduser overly redacted',
5637
+ 'self enduser retains non-redacted fields',
5638
+ )
5639
+ }
5640
+
5641
+ // verify schema-level redactions still apply to self
5583
5642
  const redactedFields = (
5584
5643
  Object.keys(schema.endusers.fields)
5585
- .filter(f =>
5644
+ .filter(f =>
5586
5645
  schema.endusers.fields[f as keyof typeof schema.endusers.fields]?.redactions?.includes('enduser')
5587
5646
  || schema.endusers.fields[f as keyof typeof schema.endusers.fields]?.redactions?.includes('all')
5588
5647
  )
5589
5648
  )
5590
5649
  assert(redactedFields.length > 0, 'no redacted fields', 'redacted fields exists')
5591
5650
 
5592
- assert(
5593
- endusers.find(e => redactedFields.filter(f => !!e[f as keyof typeof e]).length > 0) === undefined,
5594
- 'got redacted data',
5595
- 'data correctly redacted',
5596
- )
5651
+ if (selfEnduser) {
5652
+ assert(
5653
+ redactedFields.filter(f => !!(selfEnduser as any)[f]).length === 0,
5654
+ 'self enduser got redacted data',
5655
+ 'self enduser data correctly redacted per schema',
5656
+ )
5657
+ }
5597
5658
 
5598
5659
  assert(
5599
5660
  !forUser.find(u => u.hashedPassword),
@@ -13947,6 +14008,8 @@ const ip_address_form_tests = async () => {
13947
14008
  await replace_enduser_template_values_tests()
13948
14009
  await mfa_tests()
13949
14010
  await setup_tests(sdk, sdkNonAdmin)
14011
+ await get_some_projection_tests({ sdk, sdkNonAdmin })
14012
+ await elation_user_id_tests({ sdk, sdkNonAdmin })
13950
14013
  await custom_dashboards_tests({ sdk, sdkNonAdmin })
13951
14014
  await concurrent_build_threads_tests({ sdk, sdkNonAdmin })
13952
14015
  await custom_aggregation_tests({ sdk, sdkNonAdmin })
Binary file