@getstrata/core 0.5.99 → 0.5.101

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.
@@ -18,32 +18,80 @@ function belongsTo(definition) {
18
18
  ...definition
19
19
  };
20
20
  }
21
+ function hasManyThrough(definition) {
22
+ return {
23
+ type: "hasManyThrough",
24
+ throughParentKey: "__through_parent_id",
25
+ ...definition
26
+ };
27
+ }
21
28
  function belongsToMany(definition) {
22
29
  return {
23
30
  type: "belongsToMany",
24
31
  ...definition
25
32
  };
26
33
  }
34
+ function relationMatchKey(value) {
35
+ if (value === null || value === undefined) {
36
+ return "";
37
+ }
38
+ if (typeof value === "bigint") {
39
+ return value.toString();
40
+ }
41
+ if (typeof value === "number" && Number.isFinite(value)) {
42
+ return String(value);
43
+ }
44
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
45
+ return BigInt(value).toString();
46
+ }
47
+ return String(value);
48
+ }
49
+ function getByRelationKey(map, key) {
50
+ if (map.has(key)) {
51
+ return map.get(key);
52
+ }
53
+ const want = relationMatchKey(key);
54
+ if (want === "") {
55
+ return;
56
+ }
57
+ for (const [existing, value] of map) {
58
+ if (relationMatchKey(existing) === want) {
59
+ return value;
60
+ }
61
+ }
62
+ return;
63
+ }
27
64
  function indexHasManyRelation(parents, children, relation) {
28
65
  const groups = new Map;
66
+ const originalKeys = new Map;
29
67
  for (const parent of parents) {
30
- groups.set(parent[relation.localKey], []);
68
+ const key = relationMatchKey(parent[relation.localKey]);
69
+ if (!groups.has(key)) {
70
+ groups.set(key, []);
71
+ originalKeys.set(key, parent[relation.localKey]);
72
+ }
31
73
  }
32
74
  for (const child of children) {
33
- const key = child[relation.foreignKey];
34
- const group = groups.get(key);
75
+ const group = groups.get(relationMatchKey(child[relation.foreignKey]));
35
76
  if (!group) {
36
77
  continue;
37
78
  }
38
79
  group.push(child);
39
80
  }
40
- return groups;
81
+ const result = new Map;
82
+ for (const [key, group] of groups) {
83
+ const original = originalKeys.get(key);
84
+ if (original !== undefined) {
85
+ result.set(original, group);
86
+ }
87
+ }
88
+ return result;
41
89
  }
42
90
  function indexHasOneRelation(parents, children, relation) {
43
91
  const grouped = indexHasManyRelation(parents, children, relation);
44
92
  const result = new Map;
45
93
  for (const parent of parents) {
46
- const matches = grouped.get(parent[relation.localKey]) ?? [];
94
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
47
95
  result.set(parent[relation.localKey], matches[0]);
48
96
  }
49
97
  return result;
@@ -51,12 +99,12 @@ function indexHasOneRelation(parents, children, relation) {
51
99
  function indexBelongsToRelation(children, parents, relation) {
52
100
  const parentsById = new Map;
53
101
  for (const parent of parents) {
54
- parentsById.set(parent[relation.ownerKey], parent);
102
+ parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
55
103
  }
56
104
  const result = new Map;
57
105
  for (const child of children) {
58
106
  const foreignKey = child[relation.foreignKey];
59
- const parent = parentsById.get(foreignKey);
107
+ const parent = parentsById.get(relationMatchKey(foreignKey));
60
108
  if (parent) {
61
109
  result.set(foreignKey, parent);
62
110
  }
@@ -66,23 +114,33 @@ function indexBelongsToRelation(children, parents, relation) {
66
114
  function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
67
115
  const relatedById = new Map;
68
116
  for (const related of relatedRows) {
69
- relatedById.set(related[relation.relatedKey], related);
117
+ relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
70
118
  }
71
119
  const groups = new Map;
120
+ const originalKeys = new Map;
72
121
  for (const parent of parents) {
73
- groups.set(parent[relation.parentKey], []);
122
+ const key = relationMatchKey(parent[relation.parentKey]);
123
+ if (!groups.has(key)) {
124
+ groups.set(key, []);
125
+ originalKeys.set(key, parent[relation.parentKey]);
126
+ }
74
127
  }
75
128
  for (const pivot of pivotRows) {
76
- const parentId = pivot[relation.foreignPivotKey];
77
- const relatedId = pivot[relation.relatedPivotKey];
78
- const group = groups.get(parentId);
79
- const related = relatedById.get(relatedId);
129
+ const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
130
+ const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
80
131
  if (!group || !related) {
81
132
  continue;
82
133
  }
83
134
  group.push(related);
84
135
  }
85
- return groups;
136
+ const result = new Map;
137
+ for (const [key, group] of groups) {
138
+ const original = originalKeys.get(key);
139
+ if (original !== undefined) {
140
+ result.set(original, group);
141
+ }
142
+ }
143
+ return result;
86
144
  }
87
145
  function morphMany(definition) {
88
146
  return {
@@ -104,31 +162,62 @@ function morphTo(definition) {
104
162
  }
105
163
  function indexMorphManyRelation(parents, children, relation) {
106
164
  const groups = new Map;
165
+ const originalKeys = new Map;
107
166
  for (const parent of parents) {
108
- groups.set(parent[relation.localKey], []);
167
+ const key = relationMatchKey(parent[relation.localKey]);
168
+ if (!groups.has(key)) {
169
+ groups.set(key, []);
170
+ originalKeys.set(key, parent[relation.localKey]);
171
+ }
109
172
  }
110
173
  for (const child of children) {
111
174
  if (child[relation.morphTypeKey] !== relation.morphType) {
112
175
  continue;
113
176
  }
114
- const key = child[relation.morphIdKey];
115
- const group = groups.get(key);
177
+ const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
116
178
  if (!group) {
117
179
  continue;
118
180
  }
119
181
  group.push(child);
120
182
  }
121
- return groups;
183
+ const result = new Map;
184
+ for (const [key, group] of groups) {
185
+ const original = originalKeys.get(key);
186
+ if (original !== undefined) {
187
+ result.set(original, group);
188
+ }
189
+ }
190
+ return result;
122
191
  }
123
192
  function indexMorphOneRelation(parents, children, relation) {
124
193
  const grouped = indexMorphManyRelation(parents, children, relation);
125
194
  const result = new Map;
126
195
  for (const parent of parents) {
127
- const matches = grouped.get(parent[relation.localKey]) ?? [];
196
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
128
197
  result.set(parent[relation.localKey], matches[0]);
129
198
  }
130
199
  return result;
131
200
  }
201
+ function indexHasManyThroughRelation(parents, children, relation) {
202
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
203
+ const grouped = new Map;
204
+ for (const child of children) {
205
+ const key = relationMatchKey(child[throughKey]);
206
+ if (key === "") {
207
+ continue;
208
+ }
209
+ const existing = grouped.get(key) ?? [];
210
+ const { [throughKey]: _through, ...far } = child;
211
+ existing.push(far);
212
+ grouped.set(key, existing);
213
+ }
214
+ const result = new Map;
215
+ for (const parent of parents) {
216
+ const key = relationMatchKey(parent[relation.localKey]);
217
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
218
+ }
219
+ return result;
220
+ }
132
221
  function indexMorphToRelation(children, parentsByType, relation) {
133
222
  const result = new Map;
134
223
  for (const child of children) {
@@ -137,7 +226,7 @@ function indexMorphToRelation(children, parentsByType, relation) {
137
226
  if (!parents) {
138
227
  continue;
139
228
  }
140
- const parent = parents.get(child[relation.morphIdKey]);
229
+ const parent = getByRelationKey(parents, child[relation.morphIdKey]);
141
230
  if (parent) {
142
231
  result.set(child[relation.morphIdKey], parent);
143
232
  }
@@ -147,16 +236,20 @@ function indexMorphToRelation(children, parentsByType, relation) {
147
236
  export {
148
237
  belongsTo,
149
238
  belongsToMany,
239
+ getByRelationKey,
150
240
  hasMany,
241
+ hasManyThrough,
151
242
  hasOne,
152
243
  indexBelongsToManyRelation,
153
244
  indexBelongsToRelation,
154
245
  indexHasManyRelation,
246
+ indexHasManyThroughRelation,
155
247
  indexHasOneRelation,
156
248
  indexMorphManyRelation,
157
249
  indexMorphOneRelation,
158
250
  indexMorphToRelation,
159
251
  morphMany,
160
252
  morphOne,
161
- morphTo
253
+ morphTo,
254
+ relationMatchKey
162
255
  };
@@ -422,6 +422,241 @@ function buildDeleteByIdQuery(table, id) {
422
422
  };
423
423
  }
424
424
 
425
+ // ../../src/core/database/relationships.ts
426
+ function hasMany(definition) {
427
+ return {
428
+ type: "hasMany",
429
+ ...definition
430
+ };
431
+ }
432
+ function hasOne(definition) {
433
+ return {
434
+ type: "hasOne",
435
+ ...definition
436
+ };
437
+ }
438
+ function belongsTo(definition) {
439
+ return {
440
+ type: "belongsTo",
441
+ ...definition
442
+ };
443
+ }
444
+ function hasManyThrough(definition) {
445
+ return {
446
+ type: "hasManyThrough",
447
+ throughParentKey: "__through_parent_id",
448
+ ...definition
449
+ };
450
+ }
451
+ function belongsToMany(definition) {
452
+ return {
453
+ type: "belongsToMany",
454
+ ...definition
455
+ };
456
+ }
457
+ function relationMatchKey(value) {
458
+ if (value === null || value === undefined) {
459
+ return "";
460
+ }
461
+ if (typeof value === "bigint") {
462
+ return value.toString();
463
+ }
464
+ if (typeof value === "number" && Number.isFinite(value)) {
465
+ return String(value);
466
+ }
467
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
468
+ return BigInt(value).toString();
469
+ }
470
+ return String(value);
471
+ }
472
+ function getByRelationKey(map, key) {
473
+ if (map.has(key)) {
474
+ return map.get(key);
475
+ }
476
+ const want = relationMatchKey(key);
477
+ if (want === "") {
478
+ return;
479
+ }
480
+ for (const [existing, value] of map) {
481
+ if (relationMatchKey(existing) === want) {
482
+ return value;
483
+ }
484
+ }
485
+ return;
486
+ }
487
+ function indexHasManyRelation(parents, children, relation) {
488
+ const groups = new Map;
489
+ const originalKeys = new Map;
490
+ for (const parent of parents) {
491
+ const key = relationMatchKey(parent[relation.localKey]);
492
+ if (!groups.has(key)) {
493
+ groups.set(key, []);
494
+ originalKeys.set(key, parent[relation.localKey]);
495
+ }
496
+ }
497
+ for (const child of children) {
498
+ const group = groups.get(relationMatchKey(child[relation.foreignKey]));
499
+ if (!group) {
500
+ continue;
501
+ }
502
+ group.push(child);
503
+ }
504
+ const result = new Map;
505
+ for (const [key, group] of groups) {
506
+ const original = originalKeys.get(key);
507
+ if (original !== undefined) {
508
+ result.set(original, group);
509
+ }
510
+ }
511
+ return result;
512
+ }
513
+ function indexHasOneRelation(parents, children, relation) {
514
+ const grouped = indexHasManyRelation(parents, children, relation);
515
+ const result = new Map;
516
+ for (const parent of parents) {
517
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
518
+ result.set(parent[relation.localKey], matches[0]);
519
+ }
520
+ return result;
521
+ }
522
+ function indexBelongsToRelation(children, parents, relation) {
523
+ const parentsById = new Map;
524
+ for (const parent of parents) {
525
+ parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
526
+ }
527
+ const result = new Map;
528
+ for (const child of children) {
529
+ const foreignKey = child[relation.foreignKey];
530
+ const parent = parentsById.get(relationMatchKey(foreignKey));
531
+ if (parent) {
532
+ result.set(foreignKey, parent);
533
+ }
534
+ }
535
+ return result;
536
+ }
537
+ function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
538
+ const relatedById = new Map;
539
+ for (const related of relatedRows) {
540
+ relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
541
+ }
542
+ const groups = new Map;
543
+ const originalKeys = new Map;
544
+ for (const parent of parents) {
545
+ const key = relationMatchKey(parent[relation.parentKey]);
546
+ if (!groups.has(key)) {
547
+ groups.set(key, []);
548
+ originalKeys.set(key, parent[relation.parentKey]);
549
+ }
550
+ }
551
+ for (const pivot of pivotRows) {
552
+ const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
553
+ const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
554
+ if (!group || !related) {
555
+ continue;
556
+ }
557
+ group.push(related);
558
+ }
559
+ const result = new Map;
560
+ for (const [key, group] of groups) {
561
+ const original = originalKeys.get(key);
562
+ if (original !== undefined) {
563
+ result.set(original, group);
564
+ }
565
+ }
566
+ return result;
567
+ }
568
+ function morphMany(definition) {
569
+ return {
570
+ type: "morphMany",
571
+ ...definition
572
+ };
573
+ }
574
+ function morphOne(definition) {
575
+ return {
576
+ type: "morphOne",
577
+ ...definition
578
+ };
579
+ }
580
+ function morphTo(definition) {
581
+ return {
582
+ type: "morphTo",
583
+ ...definition
584
+ };
585
+ }
586
+ function indexMorphManyRelation(parents, children, relation) {
587
+ const groups = new Map;
588
+ const originalKeys = new Map;
589
+ for (const parent of parents) {
590
+ const key = relationMatchKey(parent[relation.localKey]);
591
+ if (!groups.has(key)) {
592
+ groups.set(key, []);
593
+ originalKeys.set(key, parent[relation.localKey]);
594
+ }
595
+ }
596
+ for (const child of children) {
597
+ if (child[relation.morphTypeKey] !== relation.morphType) {
598
+ continue;
599
+ }
600
+ const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
601
+ if (!group) {
602
+ continue;
603
+ }
604
+ group.push(child);
605
+ }
606
+ const result = new Map;
607
+ for (const [key, group] of groups) {
608
+ const original = originalKeys.get(key);
609
+ if (original !== undefined) {
610
+ result.set(original, group);
611
+ }
612
+ }
613
+ return result;
614
+ }
615
+ function indexMorphOneRelation(parents, children, relation) {
616
+ const grouped = indexMorphManyRelation(parents, children, relation);
617
+ const result = new Map;
618
+ for (const parent of parents) {
619
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
620
+ result.set(parent[relation.localKey], matches[0]);
621
+ }
622
+ return result;
623
+ }
624
+ function indexHasManyThroughRelation(parents, children, relation) {
625
+ const throughKey = relation.throughParentKey ?? "__through_parent_id";
626
+ const grouped = new Map;
627
+ for (const child of children) {
628
+ const key = relationMatchKey(child[throughKey]);
629
+ if (key === "") {
630
+ continue;
631
+ }
632
+ const existing = grouped.get(key) ?? [];
633
+ const { [throughKey]: _through, ...far } = child;
634
+ existing.push(far);
635
+ grouped.set(key, existing);
636
+ }
637
+ const result = new Map;
638
+ for (const parent of parents) {
639
+ const key = relationMatchKey(parent[relation.localKey]);
640
+ result.set(parent[relation.localKey], grouped.get(key) ?? []);
641
+ }
642
+ return result;
643
+ }
644
+ function indexMorphToRelation(children, parentsByType, relation) {
645
+ const result = new Map;
646
+ for (const child of children) {
647
+ const morphType = String(child[relation.morphTypeKey]);
648
+ const parents = parentsByType.get(morphType);
649
+ if (!parents) {
650
+ continue;
651
+ }
652
+ const parent = getByRelationKey(parents, child[relation.morphIdKey]);
653
+ if (parent) {
654
+ result.set(child[relation.morphIdKey], parent);
655
+ }
656
+ }
657
+ return result;
658
+ }
659
+
425
660
  // ../../src/core/database/whereBuilder.ts
426
661
  class WhereBuilder {
427
662
  nodes = [];
@@ -589,6 +824,24 @@ class RepositoryQuery {
589
824
  });
590
825
  return this;
591
826
  }
827
+ withHasManyThrough(as, relation, farRepository, options = {}) {
828
+ this.eagerLoads.push({
829
+ kind: "hasManyThrough",
830
+ as,
831
+ relation,
832
+ repository: farRepository,
833
+ options
834
+ });
835
+ return this;
836
+ }
837
+ withTrashed() {
838
+ this.queryOptions = { ...this.queryOptions, withTrashed: true };
839
+ return this;
840
+ }
841
+ onlyTrashed() {
842
+ this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
843
+ return this;
844
+ }
592
845
  async get() {
593
846
  const rows = await this.repository.findAll(this.buildOptions());
594
847
  return await this.attach(rows);
@@ -652,7 +905,7 @@ class RepositoryQuery {
652
905
  const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
653
906
  result = result.map((row) => ({
654
907
  ...row,
655
- [load.as]: grouped2.get(row[relation2.localKey]) ?? []
908
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
656
909
  }));
657
910
  continue;
658
911
  }
@@ -661,7 +914,7 @@ class RepositoryQuery {
661
914
  const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
662
915
  result = result.map((row) => ({
663
916
  ...row,
664
- [load.as]: grouped2.get(row[relation2.localKey]) ?? []
917
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
665
918
  }));
666
919
  continue;
667
920
  }
@@ -670,7 +923,16 @@ class RepositoryQuery {
670
923
  const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
671
924
  result = result.map((row) => ({
672
925
  ...row,
673
- [load.as]: grouped2.get(row[relation2.localKey])
926
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey])
927
+ }));
928
+ continue;
929
+ }
930
+ if (load.kind === "hasManyThrough") {
931
+ const relation2 = load.relation;
932
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
933
+ result = result.map((row) => ({
934
+ ...row,
935
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
674
936
  }));
675
937
  continue;
676
938
  }
@@ -679,7 +941,7 @@ class RepositoryQuery {
679
941
  const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
680
942
  result = result.map((row) => ({
681
943
  ...row,
682
- [load.as]: grouped2.get(row[relation2.parentKey]) ?? []
944
+ [load.as]: getByRelationKey(grouped2, row[relation2.parentKey]) ?? []
683
945
  }));
684
946
  continue;
685
947
  }
@@ -688,7 +950,7 @@ class RepositoryQuery {
688
950
  const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
689
951
  result = result.map((row) => ({
690
952
  ...row,
691
- [load.as]: grouped2.get(row[relation2.morphIdKey])
953
+ [load.as]: getByRelationKey(grouped2, row[relation2.morphIdKey])
692
954
  }));
693
955
  continue;
694
956
  }
@@ -696,7 +958,7 @@ class RepositoryQuery {
696
958
  const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
697
959
  result = result.map((row) => ({
698
960
  ...row,
699
- [load.as]: grouped.get(row[relation.foreignKey])
961
+ [load.as]: getByRelationKey(grouped, row[relation.foreignKey])
700
962
  }));
701
963
  }
702
964
  return result;
@@ -94,10 +94,30 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
94
94
  "POST /billing/webhooks/stripe": "Stripe webhook receiver (stub)",
95
95
  "GET /scim/v2/Users": "SCIM list users",
96
96
  "POST /scim/v2/Users": "SCIM create user",
97
- "GET /scim/v2/Groups": "SCIM list groups (organizations)",
98
97
  "GET /health": "Liveness probe",
99
98
  "GET /ready": "Readiness probe",
100
- "GET /metrics": "Prometheus metrics"
99
+ "GET /metrics": "Prometheus metrics",
100
+ "GET /api/user": "Current authenticated HiroApp user",
101
+ "POST /api/login": "Login with email and password",
102
+ "POST /api/logout": "Log out the current cookie session",
103
+ "POST /api/auth/two-factor-challenge": "Complete staff two-factor login challenge",
104
+ "GET /api/users/me": "Current user profile",
105
+ "PATCH /api/users/me": "Update profile name and email",
106
+ "PUT /api/users/me/password": "Change password",
107
+ "GET /api/users/me/sessions": "List cookie browser sessions",
108
+ "DELETE /api/users/me/sessions/:id": "Revoke one cookie browser session",
109
+ "GET /api/auth/tokens": "List API tokens",
110
+ "POST /api/auth/tokens": "Create API token",
111
+ "DELETE /api/auth/tokens/:id": "Revoke API token",
112
+ "GET /api/applications": "List applications",
113
+ "POST /api/applications": "Submit an application",
114
+ "GET /api/positions": "List hiring positions",
115
+ "GET /api/departments": "List departments",
116
+ "GET /api/webhooks": "List hiring webhooks",
117
+ "POST /api/webhooks": "Create a hiring webhook",
118
+ "GET /api/audit-logs": "List hiring audit log entries",
119
+ "GET /api/billing/subscription": "Current tenant subscription",
120
+ "GET /scim/v2/Groups": "SCIM list groups (departments)"
101
121
  };
102
122
  function toOpenApiPath(path) {
103
123
  return path.replace(/:([A-Za-z_]+)/g, "{$1}");