@getstrata/core 0.5.10 → 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
@@ -2698,18 +2866,7 @@ async function defaultSmtpTransport(config, message) {
2698
2866
  await socket.write(`DATA\r
2699
2867
  `);
2700
2868
  await waitForSmtpResponse(readResponse, ["354"]);
2701
- const payload = [
2702
- `From: ${config.from}`,
2703
- `To: ${message.to}`,
2704
- `Subject: ${message.subject}`,
2705
- "MIME-Version: 1.0",
2706
- "Content-Type: text/plain; charset=utf-8",
2707
- "",
2708
- message.body,
2709
- ".",
2710
- ""
2711
- ].join(`\r
2712
- `);
2869
+ const payload = buildSmtpPayload(config.from, message);
2713
2870
  await socket.write(payload);
2714
2871
  await waitForSmtpResponse(readResponse, ["250"]);
2715
2872
  await socket.write(`QUIT\r
@@ -2719,6 +2876,30 @@ async function defaultSmtpTransport(config, message) {
2719
2876
  socket.end();
2720
2877
  }
2721
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
+ }
2722
2903
 
2723
2904
  class LogMailDriver {
2724
2905
  async send(message) {
@@ -2727,7 +2908,8 @@ class LogMailDriver {
2727
2908
  channel: "mail",
2728
2909
  to: message.to,
2729
2910
  subject: message.subject,
2730
- body: message.body
2911
+ body: message.body,
2912
+ ...message.html ? { html: message.html } : {}
2731
2913
  }));
2732
2914
  }
2733
2915
  }
@@ -4867,6 +5049,147 @@ function resetGracefulShutdownForTests() {
4867
5049
  shutdownInstalled = false;
4868
5050
  shuttingDown = false;
4869
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
+ }
4870
5193
  // ../../src/core/queue/index.ts
4871
5194
  class Job {
4872
5195
  maxAttempts;
@@ -5370,6 +5693,7 @@ function validateObject(payload, schema) {
5370
5693
  return output;
5371
5694
  }
5372
5695
  export {
5696
+ wrapMarkdownMailLayout,
5373
5697
  withMigrationLock,
5374
5698
  withMiddleware,
5375
5699
  withErrorHandling,
@@ -5377,10 +5701,12 @@ export {
5377
5701
  validateObject,
5378
5702
  toResourceCollection,
5379
5703
  toPaginatedResourceCollection,
5704
+ stripMarkdown,
5380
5705
  stringRule,
5381
5706
  storageFacade as storage,
5382
5707
  setActiveApplicationContext,
5383
5708
  serializeDate,
5709
+ sendMarkdownMail,
5384
5710
  securedBindRouteModelByKey,
5385
5711
  securedBindRouteModel,
5386
5712
  runWithDatabaseConnection,
@@ -5403,6 +5729,7 @@ export {
5403
5729
  resolveApplicationCache,
5404
5730
  resolveApplicationAuth,
5405
5731
  required,
5732
+ renderMarkdownMail,
5406
5733
  registerShutdownHandler,
5407
5734
  registerModelRepository,
5408
5735
  readSubmittedCsrfTokenFromBody,
@@ -5416,9 +5743,13 @@ export {
5416
5743
  paginatedResponse,
5417
5744
  normalizeMetricPath,
5418
5745
  noContentResponse,
5746
+ morphTo,
5747
+ morphOne,
5748
+ morphMany,
5419
5749
  minLength,
5420
5750
  migrateDatabase,
5421
5751
  maxLength,
5752
+ markdownToHtml,
5422
5753
  mailer,
5423
5754
  mail,
5424
5755
  log,
@@ -5430,6 +5761,9 @@ export {
5430
5761
  isEtagEnabled,
5431
5762
  installGracefulShutdownSignals,
5432
5763
  inferReferencedTable,
5764
+ indexMorphToRelation,
5765
+ indexMorphOneRelation,
5766
+ indexMorphManyRelation,
5433
5767
  indexHasOneRelation,
5434
5768
  indexHasManyRelation,
5435
5769
  indexBelongsToRelation,
@@ -5459,6 +5793,7 @@ export {
5459
5793
  createQueueWorker,
5460
5794
  createQueue,
5461
5795
  createProductionQueue,
5796
+ createNotificationDispatcher,
5462
5797
  createMetricsMiddleware,
5463
5798
  createMemoryThrottleMiddleware,
5464
5799
  createLoginThrottleMiddleware,
@@ -5474,6 +5809,8 @@ export {
5474
5809
  composeMiddleware,
5475
5810
  compileBlueprint,
5476
5811
  cache,
5812
+ buildSmtpPayload,
5813
+ buildMarkdownMailMessage,
5477
5814
  bindDatabaseConnection2 as bindDatabaseConnection,
5478
5815
  belongsToMany,
5479
5816
  belongsTo,
@@ -5503,6 +5840,8 @@ export {
5503
5840
  PostgresGrammar,
5504
5841
  PolicyGate,
5505
5842
  Policy,
5843
+ NotificationDispatcher,
5844
+ Notification,
5506
5845
  NotFoundError,
5507
5846
  MySqlGrammar,
5508
5847
  Model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",