@getstrata/core 0.5.9 → 0.5.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.
package/dist/index.js CHANGED
@@ -942,6 +942,66 @@ function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
942
942
  }
943
943
  return groups;
944
944
  }
945
+ function morphMany(definition) {
946
+ return {
947
+ type: "morphMany",
948
+ ...definition
949
+ };
950
+ }
951
+ function morphOne(definition) {
952
+ return {
953
+ type: "morphOne",
954
+ ...definition
955
+ };
956
+ }
957
+ function morphTo(definition) {
958
+ return {
959
+ type: "morphTo",
960
+ ...definition
961
+ };
962
+ }
963
+ function indexMorphManyRelation(parents, children, relation) {
964
+ const groups = new Map;
965
+ for (const parent of parents) {
966
+ groups.set(parent[relation.localKey], []);
967
+ }
968
+ for (const child of children) {
969
+ if (child[relation.morphTypeKey] !== relation.morphType) {
970
+ continue;
971
+ }
972
+ const key = child[relation.morphIdKey];
973
+ const group = groups.get(key);
974
+ if (!group) {
975
+ continue;
976
+ }
977
+ group.push(child);
978
+ }
979
+ return groups;
980
+ }
981
+ function indexMorphOneRelation(parents, children, relation) {
982
+ const grouped = indexMorphManyRelation(parents, children, relation);
983
+ const result = new Map;
984
+ for (const parent of parents) {
985
+ const matches = grouped.get(parent[relation.localKey]) ?? [];
986
+ result.set(parent[relation.localKey], matches[0]);
987
+ }
988
+ return result;
989
+ }
990
+ function indexMorphToRelation(children, parentsByType, relation) {
991
+ const result = new Map;
992
+ for (const child of children) {
993
+ const morphType = String(child[relation.morphTypeKey]);
994
+ const parents = parentsByType.get(morphType);
995
+ if (!parents) {
996
+ continue;
997
+ }
998
+ const parent = parents.get(child[relation.morphIdKey]);
999
+ if (parent) {
1000
+ result.set(child[relation.morphIdKey], parent);
1001
+ }
1002
+ }
1003
+ return result;
1004
+ }
945
1005
 
946
1006
  // ../../src/config/database.ts
947
1007
  function readInteger(name, fallback) {
@@ -1145,6 +1205,37 @@ class RepositoryQuery {
1145
1205
  });
1146
1206
  return this;
1147
1207
  }
1208
+ withMorphMany(as, relation, childRepository, options = {}) {
1209
+ this.eagerLoads.push({
1210
+ kind: "morphMany",
1211
+ as,
1212
+ relation,
1213
+ repository: childRepository,
1214
+ options
1215
+ });
1216
+ return this;
1217
+ }
1218
+ withMorphOne(as, relation, childRepository, options = {}) {
1219
+ this.eagerLoads.push({
1220
+ kind: "morphOne",
1221
+ as,
1222
+ relation,
1223
+ repository: childRepository,
1224
+ options
1225
+ });
1226
+ return this;
1227
+ }
1228
+ withMorphTo(as, relation, repositoriesByType, options = {}) {
1229
+ this.eagerLoads.push({
1230
+ kind: "morphTo",
1231
+ as,
1232
+ relation,
1233
+ repository: this.repository,
1234
+ morphRepositories: repositoriesByType,
1235
+ options
1236
+ });
1237
+ return this;
1238
+ }
1148
1239
  async get() {
1149
1240
  const rows = await this.repository.findAll(this.buildOptions());
1150
1241
  return await this.attach(rows);
@@ -1205,6 +1296,33 @@ class RepositoryQuery {
1205
1296
  }));
1206
1297
  continue;
1207
1298
  }
1299
+ if (load.kind === "morphMany") {
1300
+ const relation2 = load.relation;
1301
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
1302
+ result = result.map((row) => ({
1303
+ ...row,
1304
+ [load.as]: grouped2.get(row[relation2.localKey]) ?? []
1305
+ }));
1306
+ continue;
1307
+ }
1308
+ if (load.kind === "morphOne") {
1309
+ const relation2 = load.relation;
1310
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
1311
+ result = result.map((row) => ({
1312
+ ...row,
1313
+ [load.as]: grouped2.get(row[relation2.localKey])
1314
+ }));
1315
+ continue;
1316
+ }
1317
+ if (load.kind === "morphTo") {
1318
+ const relation2 = load.relation;
1319
+ const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
1320
+ result = result.map((row) => ({
1321
+ ...row,
1322
+ [load.as]: grouped2.get(row[relation2.morphIdKey])
1323
+ }));
1324
+ continue;
1325
+ }
1208
1326
  const relation = load.relation;
1209
1327
  const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
1210
1328
  result = result.map((row) => ({
@@ -1481,6 +1599,56 @@ class BaseRepository {
1481
1599
  }, options);
1482
1600
  return indexBelongsToRelation(children, parents, relation);
1483
1601
  }
1602
+ async loadMorphManyForParents(parents, relation, options = {}) {
1603
+ if (parents.length === 0) {
1604
+ return indexMorphManyRelation(parents, [], relation);
1605
+ }
1606
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
1607
+ const children = await this.findWhere({
1608
+ [relation.morphTypeKey]: relation.morphType,
1609
+ [relation.morphIdKey]: parentIds
1610
+ }, options);
1611
+ return indexMorphManyRelation(parents, children, relation);
1612
+ }
1613
+ async loadMorphOneForParents(parents, relation, options = {}) {
1614
+ const grouped = await this.loadMorphManyForParents(parents, relation, options);
1615
+ const result = new Map;
1616
+ for (const parent of parents) {
1617
+ const matches = grouped.get(parent[relation.localKey]) ?? [];
1618
+ result.set(parent[relation.localKey], matches[0]);
1619
+ }
1620
+ return result;
1621
+ }
1622
+ async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
1623
+ if (children.length === 0) {
1624
+ return new Map;
1625
+ }
1626
+ const idsByType = new Map;
1627
+ for (const child of children) {
1628
+ const morphType = String(child[relation.morphTypeKey]);
1629
+ const morphId = child[relation.morphIdKey];
1630
+ const ids = idsByType.get(morphType) ?? new Set;
1631
+ ids.add(morphId);
1632
+ idsByType.set(morphType, ids);
1633
+ }
1634
+ const parentsByType = new Map;
1635
+ for (const [morphType, ids] of idsByType) {
1636
+ const repository = repositoriesByType.get(morphType);
1637
+ if (!repository) {
1638
+ continue;
1639
+ }
1640
+ const ownerKey = repository.getTable().primaryKey;
1641
+ const parents = await repository.withConnection(this.connection).findWhere({
1642
+ [ownerKey]: [...ids]
1643
+ }, options);
1644
+ const indexed = new Map;
1645
+ for (const parent of parents) {
1646
+ indexed.set(parent[ownerKey], parent);
1647
+ }
1648
+ parentsByType.set(morphType, indexed);
1649
+ }
1650
+ return indexMorphToRelation(children, parentsByType, relation);
1651
+ }
1484
1652
  }
1485
1653
  var baseRepository_default = BaseRepository;
1486
1654
  // ../../src/core/database/bindConnection.ts
@@ -2573,8 +2741,15 @@ function defineTable(definition) {
2573
2741
  return definition;
2574
2742
  }
2575
2743
  // ../../src/core/database/transaction.ts
2744
+ function supportsTransactions(connection) {
2745
+ return typeof connection.begin === "function";
2746
+ }
2576
2747
  async function runInTransaction(operation) {
2577
- return await connection_default.begin(async (transaction) => {
2748
+ const pool = resolveRepositoryConnection();
2749
+ if (!supportsTransactions(pool)) {
2750
+ throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
2751
+ }
2752
+ return await pool.begin(async (transaction) => {
2578
2753
  return await operation(createDatabaseConnection2(transaction));
2579
2754
  });
2580
2755
  }
@@ -2691,18 +2866,7 @@ async function defaultSmtpTransport(config, message) {
2691
2866
  await socket.write(`DATA\r
2692
2867
  `);
2693
2868
  await waitForSmtpResponse(readResponse, ["354"]);
2694
- const payload = [
2695
- `From: ${config.from}`,
2696
- `To: ${message.to}`,
2697
- `Subject: ${message.subject}`,
2698
- "MIME-Version: 1.0",
2699
- "Content-Type: text/plain; charset=utf-8",
2700
- "",
2701
- message.body,
2702
- ".",
2703
- ""
2704
- ].join(`\r
2705
- `);
2869
+ const payload = buildSmtpPayload(config.from, message);
2706
2870
  await socket.write(payload);
2707
2871
  await waitForSmtpResponse(readResponse, ["250"]);
2708
2872
  await socket.write(`QUIT\r
@@ -2712,6 +2876,30 @@ async function defaultSmtpTransport(config, message) {
2712
2876
  socket.end();
2713
2877
  }
2714
2878
  }
2879
+ function buildSmtpPayload(from, message) {
2880
+ const headers = [`From: ${from}`, `To: ${message.to}`, `Subject: ${message.subject}`, "MIME-Version: 1.0"];
2881
+ if (message.html) {
2882
+ const boundary = `strata-${Date.now().toString(36)}`;
2883
+ headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
2884
+ const parts = [
2885
+ `--${boundary}`,
2886
+ "Content-Type: text/plain; charset=utf-8",
2887
+ "",
2888
+ message.body,
2889
+ `--${boundary}`,
2890
+ "Content-Type: text/html; charset=utf-8",
2891
+ "",
2892
+ message.html,
2893
+ `--${boundary}--`,
2894
+ ""
2895
+ ];
2896
+ return [...headers, "", ...parts, ".", ""].join(`\r
2897
+ `);
2898
+ }
2899
+ headers.push("Content-Type: text/plain; charset=utf-8");
2900
+ return [...headers, "", message.body, ".", ""].join(`\r
2901
+ `);
2902
+ }
2715
2903
 
2716
2904
  class LogMailDriver {
2717
2905
  async send(message) {
@@ -2720,7 +2908,8 @@ class LogMailDriver {
2720
2908
  channel: "mail",
2721
2909
  to: message.to,
2722
2910
  subject: message.subject,
2723
- body: message.body
2911
+ body: message.body,
2912
+ ...message.html ? { html: message.html } : {}
2724
2913
  }));
2725
2914
  }
2726
2915
  }
@@ -4860,6 +5049,147 @@ function resetGracefulShutdownForTests() {
4860
5049
  shutdownInstalled = false;
4861
5050
  shuttingDown = false;
4862
5051
  }
5052
+ // ../../src/core/mail/markdownMail.ts
5053
+ function escapeHtml(value) {
5054
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5055
+ }
5056
+ function stripMarkdown(markdown) {
5057
+ return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/^[-*]\s+/gm, "\u2022 ").replace(/```[\s\S]*?```/g, "").replace(/\n{3,}/g, `
5058
+
5059
+ `).trim();
5060
+ }
5061
+ function markdownToHtml(markdown) {
5062
+ const escaped = markdown.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5063
+ return escaped.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>').replace(/^[-*]\s+(.+)$/gm, "<li>$1</li>").replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (block) => `<ul>${block}</ul>`).replace(/```(\w+)?\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>").split(/\n\n+/).map((block) => {
5064
+ if (block.startsWith("<")) {
5065
+ return block;
5066
+ }
5067
+ return `<p>${block.replace(/\n/g, " ")}</p>`;
5068
+ }).join(`
5069
+ `);
5070
+ }
5071
+ function wrapMarkdownMailLayout(bodyHtml, options = {}) {
5072
+ const title = escapeHtml(options.title ?? "GetStrata");
5073
+ const preview = escapeHtml(options.preview ?? "");
5074
+ const footer = escapeHtml(options.footer ?? "Sent by GetStrata");
5075
+ return `<!DOCTYPE html>
5076
+ <html lang="en">
5077
+ <head>
5078
+ <meta charset="utf-8">
5079
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5080
+ <title>${title}</title>
5081
+ <style>
5082
+ body { font-family: system-ui, sans-serif; line-height: 1.5; color: #111827; background: #f9fafb; margin: 0; padding: 24px; }
5083
+ .container { max-width: 640px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
5084
+ .header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; font-weight: 600; }
5085
+ .content { padding: 24px; }
5086
+ .footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px; }
5087
+ a { color: #2563eb; }
5088
+ code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
5089
+ pre { background: #111827; color: #f9fafb; padding: 12px; border-radius: 6px; overflow-x: auto; }
5090
+ </style>
5091
+ </head>
5092
+ <body>
5093
+ ${preview ? `<span style="display:none;max-height:0;overflow:hidden;">${preview}</span>` : ""}
5094
+ <div class="container">
5095
+ <div class="header">${title}</div>
5096
+ <div class="content">${bodyHtml}</div>
5097
+ <div class="footer">${footer}</div>
5098
+ </div>
5099
+ </body>
5100
+ </html>`;
5101
+ }
5102
+ function renderMarkdownMail(markdown, options = {}) {
5103
+ const bodyHtml = markdownToHtml(markdown.trim());
5104
+ const html = wrapMarkdownMailLayout(bodyHtml, options);
5105
+ const text = stripMarkdown(markdown);
5106
+ return { html, text };
5107
+ }
5108
+ // ../../src/core/mail/markdownMailable.ts
5109
+ function buildMarkdownMailMessage(input) {
5110
+ const rendered = renderMarkdownMail(input.markdown, {
5111
+ title: input.layout?.title ?? input.subject,
5112
+ ...input.layout
5113
+ });
5114
+ return {
5115
+ to: input.to,
5116
+ subject: input.subject,
5117
+ body: rendered.text,
5118
+ html: rendered.html
5119
+ };
5120
+ }
5121
+ async function sendMarkdownMail(mailer2, input) {
5122
+ await mailer2.send(buildMarkdownMailMessage(input));
5123
+ }
5124
+ // ../../src/core/notifications/dispatcher.ts
5125
+ class NotificationDispatcher {
5126
+ mailer;
5127
+ databaseStore;
5128
+ constructor(mailer2, databaseStore = null) {
5129
+ this.mailer = mailer2;
5130
+ this.databaseStore = databaseStore;
5131
+ }
5132
+ async send(notifiable, notification) {
5133
+ for (const channel of notification.via(notifiable)) {
5134
+ if (channel === "mail") {
5135
+ await this.sendMail(notifiable, notification);
5136
+ continue;
5137
+ }
5138
+ if (channel === "database") {
5139
+ await this.sendDatabase(notifiable, notification);
5140
+ }
5141
+ }
5142
+ }
5143
+ async sendMail(notifiable, notification) {
5144
+ const routed = notifiable.routeNotificationFor("mail");
5145
+ if (routed === null) {
5146
+ return;
5147
+ }
5148
+ const message = notification.toMail(notifiable);
5149
+ if (!message) {
5150
+ return;
5151
+ }
5152
+ if (message.markdown) {
5153
+ await this.mailer.send(buildMarkdownMailMessage({
5154
+ to: String(routed),
5155
+ subject: message.subject,
5156
+ markdown: message.markdown
5157
+ }));
5158
+ return;
5159
+ }
5160
+ await this.mailer.send({
5161
+ to: String(routed),
5162
+ subject: message.subject,
5163
+ body: message.body ?? "",
5164
+ ...message.html ? { html: message.html } : {}
5165
+ });
5166
+ }
5167
+ async sendDatabase(notifiable, notification) {
5168
+ if (!this.databaseStore) {
5169
+ return;
5170
+ }
5171
+ const payload = notification.toDatabase(notifiable);
5172
+ if (!payload) {
5173
+ return;
5174
+ }
5175
+ await this.databaseStore.create({
5176
+ userId: Number(notifiable.getNotificationKey()),
5177
+ ...payload
5178
+ });
5179
+ }
5180
+ }
5181
+ function createNotificationDispatcher(mailer2, databaseStore) {
5182
+ return new NotificationDispatcher(mailer2, databaseStore ?? null);
5183
+ }
5184
+ // ../../src/core/notifications/notification.ts
5185
+ class Notification {
5186
+ toMail(_notifiable) {
5187
+ return null;
5188
+ }
5189
+ toDatabase(_notifiable) {
5190
+ return null;
5191
+ }
5192
+ }
4863
5193
  // ../../src/core/queue/index.ts
4864
5194
  class Job {
4865
5195
  maxAttempts;
@@ -5363,6 +5693,7 @@ function validateObject(payload, schema) {
5363
5693
  return output;
5364
5694
  }
5365
5695
  export {
5696
+ wrapMarkdownMailLayout,
5366
5697
  withMigrationLock,
5367
5698
  withMiddleware,
5368
5699
  withErrorHandling,
@@ -5370,10 +5701,12 @@ export {
5370
5701
  validateObject,
5371
5702
  toResourceCollection,
5372
5703
  toPaginatedResourceCollection,
5704
+ stripMarkdown,
5373
5705
  stringRule,
5374
5706
  storageFacade as storage,
5375
5707
  setActiveApplicationContext,
5376
5708
  serializeDate,
5709
+ sendMarkdownMail,
5377
5710
  securedBindRouteModelByKey,
5378
5711
  securedBindRouteModel,
5379
5712
  runWithDatabaseConnection,
@@ -5396,6 +5729,7 @@ export {
5396
5729
  resolveApplicationCache,
5397
5730
  resolveApplicationAuth,
5398
5731
  required,
5732
+ renderMarkdownMail,
5399
5733
  registerShutdownHandler,
5400
5734
  registerModelRepository,
5401
5735
  readSubmittedCsrfTokenFromBody,
@@ -5409,9 +5743,13 @@ export {
5409
5743
  paginatedResponse,
5410
5744
  normalizeMetricPath,
5411
5745
  noContentResponse,
5746
+ morphTo,
5747
+ morphOne,
5748
+ morphMany,
5412
5749
  minLength,
5413
5750
  migrateDatabase,
5414
5751
  maxLength,
5752
+ markdownToHtml,
5415
5753
  mailer,
5416
5754
  mail,
5417
5755
  log,
@@ -5423,6 +5761,9 @@ export {
5423
5761
  isEtagEnabled,
5424
5762
  installGracefulShutdownSignals,
5425
5763
  inferReferencedTable,
5764
+ indexMorphToRelation,
5765
+ indexMorphOneRelation,
5766
+ indexMorphManyRelation,
5426
5767
  indexHasOneRelation,
5427
5768
  indexHasManyRelation,
5428
5769
  indexBelongsToRelation,
@@ -5452,6 +5793,7 @@ export {
5452
5793
  createQueueWorker,
5453
5794
  createQueue,
5454
5795
  createProductionQueue,
5796
+ createNotificationDispatcher,
5455
5797
  createMetricsMiddleware,
5456
5798
  createMemoryThrottleMiddleware,
5457
5799
  createLoginThrottleMiddleware,
@@ -5467,6 +5809,8 @@ export {
5467
5809
  composeMiddleware,
5468
5810
  compileBlueprint,
5469
5811
  cache,
5812
+ buildSmtpPayload,
5813
+ buildMarkdownMailMessage,
5470
5814
  bindDatabaseConnection2 as bindDatabaseConnection,
5471
5815
  belongsToMany,
5472
5816
  belongsTo,
@@ -5496,6 +5840,8 @@ export {
5496
5840
  PostgresGrammar,
5497
5841
  PolicyGate,
5498
5842
  Policy,
5843
+ NotificationDispatcher,
5844
+ Notification,
5499
5845
  NotFoundError,
5500
5846
  MySqlGrammar,
5501
5847
  Model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.9",
3
+ "version": "0.5.12",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",