@stravigor/core 0.1.0 → 0.2.0
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/package.json +1 -1
- package/src/auth/auth.ts +2 -1
- package/src/broadcast/broadcast_manager.ts +18 -5
- package/src/broadcast/client.ts +10 -4
- package/src/cache/cache_manager.ts +6 -2
- package/src/cache/http_cache.ts +1 -5
- package/src/core/container.ts +2 -6
- package/src/database/database.ts +11 -7
- package/src/database/migration/runner.ts +3 -1
- package/src/database/query_builder.ts +553 -60
- package/src/encryption/encryption_manager.ts +7 -1
- package/src/exceptions/errors.ts +1 -5
- package/src/exceptions/http_exception.ts +4 -1
- package/src/generators/api_generator.ts +8 -1
- package/src/generators/doc_generator.ts +33 -28
- package/src/generators/model_generator.ts +3 -1
- package/src/generators/test_generator.ts +81 -91
- package/src/i18n/helpers.ts +5 -1
- package/src/i18n/i18n_manager.ts +3 -1
- package/src/i18n/middleware.ts +2 -8
- package/src/mail/helpers.ts +1 -1
- package/src/mail/index.ts +4 -0
- package/src/mail/mail_manager.ts +20 -1
- package/src/mail/transports/alibaba_transport.ts +88 -0
- package/src/mail/transports/mailgun_transport.ts +74 -0
- package/src/mail/transports/resend_transport.ts +3 -4
- package/src/mail/transports/sendgrid_transport.ts +12 -9
- package/src/mail/transports/smtp_transport.ts +5 -5
- package/src/mail/types.ts +19 -1
- package/src/notification/channels/discord_channel.ts +6 -1
- package/src/notification/channels/webhook_channel.ts +8 -3
- package/src/notification/helpers.ts +7 -7
- package/src/notification/notification_manager.ts +7 -6
- package/src/orm/base_model.ts +4 -2
- package/src/queue/queue.ts +3 -1
- package/src/scheduler/cron.ts +12 -6
- package/src/scheduler/schedule.ts +17 -8
- package/src/session/session_manager.ts +3 -1
- package/src/storage/storage_manager.ts +3 -1
- package/src/view/compiler.ts +1 -3
- package/src/view/islands/island_builder.ts +4 -4
- package/src/view/islands/vue_plugin.ts +11 -15
- package/src/view/tokenizer.ts +11 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { ExternalServiceError } from '../../exceptions/errors.ts'
|
|
3
|
+
import type { MailTransport, MailMessage, MailResult, AlibabaConfig } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Alibaba Cloud DirectMail (SingleSendMail) transport.
|
|
7
|
+
* Uses fetch with HMAC-SHA1 signature — no SDK dependency required.
|
|
8
|
+
*
|
|
9
|
+
* Note: SingleSendMail does not support CC, BCC, or attachments.
|
|
10
|
+
* Use the SMTP interface for those features.
|
|
11
|
+
*
|
|
12
|
+
* @see https://www.alibabacloud.com/help/en/directmail/latest/SingleSendMail
|
|
13
|
+
*/
|
|
14
|
+
export class AlibabaTransport implements MailTransport {
|
|
15
|
+
private accessKeyId: string
|
|
16
|
+
private accessKeySecret: string
|
|
17
|
+
private accountName: string
|
|
18
|
+
private region: string
|
|
19
|
+
|
|
20
|
+
constructor(config: AlibabaConfig) {
|
|
21
|
+
this.accessKeyId = config.accessKeyId
|
|
22
|
+
this.accessKeySecret = config.accessKeySecret
|
|
23
|
+
this.accountName = config.accountName
|
|
24
|
+
this.region = config.region ?? 'cn-hangzhou'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async send(message: MailMessage): Promise<MailResult> {
|
|
28
|
+
const toAddress = Array.isArray(message.to) ? message.to.join(',') : message.to
|
|
29
|
+
|
|
30
|
+
const params: Record<string, string> = {
|
|
31
|
+
Action: 'SingleSendMail',
|
|
32
|
+
AccountName: this.accountName,
|
|
33
|
+
AddressType: '1',
|
|
34
|
+
ReplyToAddress: message.replyTo ? 'true' : 'false',
|
|
35
|
+
ToAddress: toAddress,
|
|
36
|
+
Subject: message.subject,
|
|
37
|
+
Format: 'JSON',
|
|
38
|
+
Version: '2015-11-23',
|
|
39
|
+
AccessKeyId: this.accessKeyId,
|
|
40
|
+
SignatureMethod: 'HMAC-SHA1',
|
|
41
|
+
SignatureVersion: '1.0',
|
|
42
|
+
SignatureNonce: crypto.randomUUID(),
|
|
43
|
+
Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (message.html) params.HtmlBody = message.html
|
|
47
|
+
if (message.text) params.TextBody = message.text
|
|
48
|
+
if (message.from) params.FromAlias = message.from
|
|
49
|
+
|
|
50
|
+
const signature = this.sign(params)
|
|
51
|
+
params.Signature = signature
|
|
52
|
+
|
|
53
|
+
const body = new URLSearchParams(params).toString()
|
|
54
|
+
const url = `https://dm.${this.region}.aliyuncs.com`
|
|
55
|
+
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
59
|
+
body,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const error = await response.text()
|
|
64
|
+
throw new ExternalServiceError('Alibaba DirectMail', response.status, error)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = (await response.json()) as { EnvId?: string; RequestId?: string }
|
|
68
|
+
const toArray = Array.isArray(message.to) ? message.to : [message.to]
|
|
69
|
+
return { messageId: data.EnvId ?? data.RequestId, accepted: toArray }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private percentEncode(str: string): string {
|
|
73
|
+
return encodeURIComponent(str).replace(/\+/g, '%20').replace(/\*/g, '%2A').replace(/~/g, '%7E')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private sign(params: Record<string, string>): string {
|
|
77
|
+
const sortedKeys = Object.keys(params).sort()
|
|
78
|
+
const canonicalized = sortedKeys
|
|
79
|
+
.map(k => `${this.percentEncode(k)}=${this.percentEncode(params[k]!)}`)
|
|
80
|
+
.join('&')
|
|
81
|
+
|
|
82
|
+
const stringToSign = `POST&${this.percentEncode('/')}&${this.percentEncode(canonicalized)}`
|
|
83
|
+
|
|
84
|
+
const hmac = crypto.createHmac('sha1', `${this.accessKeySecret}&`)
|
|
85
|
+
hmac.update(stringToSign)
|
|
86
|
+
return hmac.digest('base64')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ExternalServiceError } from '../../exceptions/errors.ts'
|
|
2
|
+
import type { MailTransport, MailMessage, MailResult, MailgunConfig } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mailgun HTTP API transport.
|
|
6
|
+
* Uses fetch with FormData — no SDK dependency required.
|
|
7
|
+
*
|
|
8
|
+
* @see https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Messages/
|
|
9
|
+
*/
|
|
10
|
+
export class MailgunTransport implements MailTransport {
|
|
11
|
+
private apiKey: string
|
|
12
|
+
private domain: string
|
|
13
|
+
private baseUrl: string
|
|
14
|
+
|
|
15
|
+
constructor(config: MailgunConfig) {
|
|
16
|
+
this.apiKey = config.apiKey
|
|
17
|
+
this.domain = config.domain
|
|
18
|
+
this.baseUrl = config.baseUrl ?? 'https://api.mailgun.net'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async send(message: MailMessage): Promise<MailResult> {
|
|
22
|
+
const form = new FormData()
|
|
23
|
+
|
|
24
|
+
form.append('from', message.from)
|
|
25
|
+
form.append('subject', message.subject)
|
|
26
|
+
|
|
27
|
+
const toArray = Array.isArray(message.to) ? message.to : [message.to]
|
|
28
|
+
form.append('to', toArray.join(', '))
|
|
29
|
+
|
|
30
|
+
if (message.cc) {
|
|
31
|
+
const ccArray = Array.isArray(message.cc) ? message.cc : [message.cc]
|
|
32
|
+
form.append('cc', ccArray.join(', '))
|
|
33
|
+
}
|
|
34
|
+
if (message.bcc) {
|
|
35
|
+
const bccArray = Array.isArray(message.bcc) ? message.bcc : [message.bcc]
|
|
36
|
+
form.append('bcc', bccArray.join(', '))
|
|
37
|
+
}
|
|
38
|
+
if (message.replyTo) form.append('h:Reply-To', message.replyTo)
|
|
39
|
+
if (message.html) form.append('html', message.html)
|
|
40
|
+
if (message.text) form.append('text', message.text)
|
|
41
|
+
|
|
42
|
+
if (message.attachments?.length) {
|
|
43
|
+
for (const a of message.attachments) {
|
|
44
|
+
const content =
|
|
45
|
+
typeof a.content === 'string'
|
|
46
|
+
? new Blob([a.content], { type: a.contentType ?? 'application/octet-stream' })
|
|
47
|
+
: new Blob([a.content], { type: a.contentType ?? 'application/octet-stream' })
|
|
48
|
+
|
|
49
|
+
if (a.cid) {
|
|
50
|
+
form.append('inline', content, a.filename)
|
|
51
|
+
} else {
|
|
52
|
+
form.append('attachment', content, a.filename)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const credentials = btoa(`api:${this.apiKey}`)
|
|
58
|
+
const response = await fetch(`${this.baseUrl}/v3/${this.domain}/messages`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Basic ${credentials}`,
|
|
62
|
+
},
|
|
63
|
+
body: form,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const error = await response.text()
|
|
68
|
+
throw new ExternalServiceError('Mailgun', response.status, error)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = (await response.json()) as { id: string }
|
|
72
|
+
return { messageId: data.id, accepted: toArray }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -30,11 +30,10 @@ export class ResendTransport implements MailTransport {
|
|
|
30
30
|
if (message.text) body.text = message.text
|
|
31
31
|
|
|
32
32
|
if (message.attachments?.length) {
|
|
33
|
-
body.attachments = message.attachments.map(
|
|
33
|
+
body.attachments = message.attachments.map(a => ({
|
|
34
34
|
filename: a.filename,
|
|
35
|
-
content:
|
|
36
|
-
? a.content
|
|
37
|
-
: Buffer.from(a.content).toString('base64'),
|
|
35
|
+
content:
|
|
36
|
+
typeof a.content === 'string' ? a.content : Buffer.from(a.content).toString('base64'),
|
|
38
37
|
content_type: a.contentType,
|
|
39
38
|
}))
|
|
40
39
|
}
|
|
@@ -19,17 +19,19 @@ export class SendGridTransport implements MailTransport {
|
|
|
19
19
|
async send(message: MailMessage): Promise<MailResult> {
|
|
20
20
|
const toArray = Array.isArray(message.to) ? message.to : [message.to]
|
|
21
21
|
|
|
22
|
-
const personalizations: Record<string, unknown>[] = [
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const personalizations: Record<string, unknown>[] = [
|
|
23
|
+
{
|
|
24
|
+
to: toArray.map(email => ({ email })),
|
|
25
|
+
},
|
|
26
|
+
]
|
|
25
27
|
|
|
26
28
|
if (message.cc) {
|
|
27
29
|
const ccArray = Array.isArray(message.cc) ? message.cc : [message.cc]
|
|
28
|
-
personalizations[0]!.cc = ccArray.map(
|
|
30
|
+
personalizations[0]!.cc = ccArray.map(email => ({ email }))
|
|
29
31
|
}
|
|
30
32
|
if (message.bcc) {
|
|
31
33
|
const bccArray = Array.isArray(message.bcc) ? message.bcc : [message.bcc]
|
|
32
|
-
personalizations[0]!.bcc = bccArray.map(
|
|
34
|
+
personalizations[0]!.bcc = bccArray.map(email => ({ email }))
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const content: { type: string; value: string }[] = []
|
|
@@ -46,11 +48,12 @@ export class SendGridTransport implements MailTransport {
|
|
|
46
48
|
if (message.replyTo) body.reply_to = { email: message.replyTo }
|
|
47
49
|
|
|
48
50
|
if (message.attachments?.length) {
|
|
49
|
-
body.attachments = message.attachments.map(
|
|
51
|
+
body.attachments = message.attachments.map(a => ({
|
|
50
52
|
filename: a.filename,
|
|
51
|
-
content:
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
content:
|
|
54
|
+
typeof a.content === 'string'
|
|
55
|
+
? Buffer.from(a.content).toString('base64')
|
|
56
|
+
: Buffer.from(a.content).toString('base64'),
|
|
54
57
|
type: a.contentType,
|
|
55
58
|
disposition: a.cid ? 'inline' : 'attachment',
|
|
56
59
|
content_id: a.cid,
|
|
@@ -21,17 +21,17 @@ export class SmtpTransport implements MailTransport {
|
|
|
21
21
|
const info = await this.transporter.sendMail({
|
|
22
22
|
from: message.from,
|
|
23
23
|
to: Array.isArray(message.to) ? message.to.join(', ') : message.to,
|
|
24
|
-
cc: message.cc
|
|
25
|
-
? Array.isArray(message.cc) ? message.cc.join(', ') : message.cc
|
|
26
|
-
: undefined,
|
|
24
|
+
cc: message.cc ? (Array.isArray(message.cc) ? message.cc.join(', ') : message.cc) : undefined,
|
|
27
25
|
bcc: message.bcc
|
|
28
|
-
? Array.isArray(message.bcc)
|
|
26
|
+
? Array.isArray(message.bcc)
|
|
27
|
+
? message.bcc.join(', ')
|
|
28
|
+
: message.bcc
|
|
29
29
|
: undefined,
|
|
30
30
|
replyTo: message.replyTo,
|
|
31
31
|
subject: message.subject,
|
|
32
32
|
html: message.html,
|
|
33
33
|
text: message.text,
|
|
34
|
-
attachments: message.attachments?.map(
|
|
34
|
+
attachments: message.attachments?.map(a => ({
|
|
35
35
|
filename: a.filename,
|
|
36
36
|
content: a.content,
|
|
37
37
|
contentType: a.contentType,
|
package/src/mail/types.ts
CHANGED
|
@@ -55,6 +55,22 @@ export interface SendGridConfig {
|
|
|
55
55
|
baseUrl?: string
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface MailgunConfig {
|
|
59
|
+
apiKey: string
|
|
60
|
+
domain: string
|
|
61
|
+
/** Default: 'https://api.mailgun.net'. EU: 'https://api.eu.mailgun.net' */
|
|
62
|
+
baseUrl?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AlibabaConfig {
|
|
66
|
+
accessKeyId: string
|
|
67
|
+
accessKeySecret: string
|
|
68
|
+
/** Sender address configured in Alibaba DirectMail. */
|
|
69
|
+
accountName: string
|
|
70
|
+
/** Default: 'cn-hangzhou' */
|
|
71
|
+
region?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
export interface LogConfig {
|
|
59
75
|
/** Write to 'console' or a file path. */
|
|
60
76
|
output: 'console' | string
|
|
@@ -63,7 +79,7 @@ export interface LogConfig {
|
|
|
63
79
|
// -- Top-level mail config ----------------------------------------------------
|
|
64
80
|
|
|
65
81
|
export interface MailConfig {
|
|
66
|
-
/** Default transport name: 'smtp' | 'resend' | 'sendgrid' | 'log' */
|
|
82
|
+
/** Default transport name: 'smtp' | 'resend' | 'sendgrid' | 'mailgun' | 'alibaba' | 'log' */
|
|
67
83
|
default: string
|
|
68
84
|
/** Default "from" address. */
|
|
69
85
|
from: string
|
|
@@ -76,5 +92,7 @@ export interface MailConfig {
|
|
|
76
92
|
smtp: SmtpConfig
|
|
77
93
|
resend: ResendConfig
|
|
78
94
|
sendgrid: SendGridConfig
|
|
95
|
+
mailgun: MailgunConfig
|
|
96
|
+
alibaba: AlibabaConfig
|
|
79
97
|
log: LogConfig
|
|
80
98
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ExternalServiceError } from '../../exceptions/errors.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
NotificationChannel,
|
|
4
|
+
Notifiable,
|
|
5
|
+
NotificationPayload,
|
|
6
|
+
NotificationConfig,
|
|
7
|
+
} from '../types.ts'
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Delivers notifications via Discord webhook.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ExternalServiceError } from '../../exceptions/errors.ts'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
NotificationChannel,
|
|
4
|
+
Notifiable,
|
|
5
|
+
NotificationPayload,
|
|
6
|
+
NotificationConfig,
|
|
7
|
+
} from '../types.ts'
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Delivers notifications via HTTP POST to a webhook URL.
|
|
@@ -28,8 +33,8 @@ export class WebhookChannel implements NotificationChannel {
|
|
|
28
33
|
|
|
29
34
|
const headers: Record<string, string> = {
|
|
30
35
|
'Content-Type': 'application/json',
|
|
31
|
-
...
|
|
32
|
-
...
|
|
36
|
+
...this.config.webhooks?.default?.headers,
|
|
37
|
+
...envelope.headers,
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const response = await fetch(url, {
|
|
@@ -16,7 +16,7 @@ import Database from '../database/database.ts'
|
|
|
16
16
|
*/
|
|
17
17
|
export async function notify(
|
|
18
18
|
notifiable: Notifiable | Notifiable[],
|
|
19
|
-
notification: BaseNotification
|
|
19
|
+
notification: BaseNotification
|
|
20
20
|
): Promise<void> {
|
|
21
21
|
const recipients = Array.isArray(notifiable) ? notifiable : [notifiable]
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ export async function notify(
|
|
|
44
44
|
queue: notification.queueOptions().queue ?? NotificationManager.config.queue,
|
|
45
45
|
delay: notification.queueOptions().delay,
|
|
46
46
|
attempts: notification.queueOptions().attempts,
|
|
47
|
-
}
|
|
47
|
+
}
|
|
48
48
|
)
|
|
49
49
|
} else {
|
|
50
50
|
await sendNow(recipient, notification)
|
|
@@ -187,10 +187,7 @@ export const notifications = {
|
|
|
187
187
|
for (const binding of bindings) {
|
|
188
188
|
const notification = binding.create(eventPayload)
|
|
189
189
|
const recipients = await binding.recipients(eventPayload)
|
|
190
|
-
await notify(
|
|
191
|
-
Array.isArray(recipients) ? recipients : [recipients],
|
|
192
|
-
notification,
|
|
193
|
-
)
|
|
190
|
+
await notify(Array.isArray(recipients) ? recipients : [recipients], notification)
|
|
194
191
|
}
|
|
195
192
|
})
|
|
196
193
|
}
|
|
@@ -207,7 +204,10 @@ function hydrateNotification(row: Record<string, unknown>): NotificationRecord {
|
|
|
207
204
|
notifiableType: row.notifiable_type as string,
|
|
208
205
|
notifiableId: row.notifiable_id as string,
|
|
209
206
|
type: row.type as string,
|
|
210
|
-
data: (typeof row.data === 'string' ? JSON.parse(row.data) : row.data) as Record<
|
|
207
|
+
data: (typeof row.data === 'string' ? JSON.parse(row.data) : row.data) as Record<
|
|
208
|
+
string,
|
|
209
|
+
unknown
|
|
210
|
+
>,
|
|
211
211
|
readAt: (row.read_at as Date) ?? null,
|
|
212
212
|
createdAt: row.created_at as Date,
|
|
213
213
|
}
|
|
@@ -32,10 +32,13 @@ export default class NotificationManager {
|
|
|
32
32
|
constructor(db: Database, config: Configuration) {
|
|
33
33
|
NotificationManager._db = db
|
|
34
34
|
NotificationManager._config = {
|
|
35
|
-
channels:
|
|
35
|
+
channels: config.get('notification.channels', ['database']) as string[],
|
|
36
36
|
queue: config.get('notification.queue', 'default') as string,
|
|
37
|
-
webhooks:
|
|
38
|
-
|
|
37
|
+
webhooks: config.get('notification.webhooks', {}) as Record<
|
|
38
|
+
string,
|
|
39
|
+
{ url: string; headers?: Record<string, string> }
|
|
40
|
+
>,
|
|
41
|
+
discord: config.get('notification.discord', {}) as Record<string, string>,
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
// Register built-in channels
|
|
@@ -47,9 +50,7 @@ export default class NotificationManager {
|
|
|
47
50
|
|
|
48
51
|
static get db(): Database {
|
|
49
52
|
if (!NotificationManager._db) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
'NotificationManager not configured. Resolve it through the container first.'
|
|
52
|
-
)
|
|
53
|
+
throw new Error('NotificationManager not configured. Resolve it through the container first.')
|
|
53
54
|
}
|
|
54
55
|
return NotificationManager._db
|
|
55
56
|
}
|
package/src/orm/base_model.ts
CHANGED
|
@@ -22,7 +22,9 @@ export default class BaseModel {
|
|
|
22
22
|
/** The underlying database connection. */
|
|
23
23
|
static get db(): Database {
|
|
24
24
|
if (!BaseModel._db) {
|
|
25
|
-
throw new ConfigurationError(
|
|
25
|
+
throw new ConfigurationError(
|
|
26
|
+
'Database not configured. Resolve BaseModel through the container first.'
|
|
27
|
+
)
|
|
26
28
|
}
|
|
27
29
|
return BaseModel._db
|
|
28
30
|
}
|
|
@@ -341,7 +343,7 @@ export default class BaseModel {
|
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
/** Convert a raw DB row to a plain object with camelCase keys and DateTime hydration. */
|
|
344
|
-
function hydrateRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
346
|
+
export function hydrateRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
345
347
|
const obj: Record<string, unknown> = {}
|
|
346
348
|
for (const [column, value] of Object.entries(row)) {
|
|
347
349
|
const prop = toCamelCase(column)
|
package/src/queue/queue.ts
CHANGED
|
@@ -86,7 +86,9 @@ export default class Queue {
|
|
|
86
86
|
|
|
87
87
|
static get db(): Database {
|
|
88
88
|
if (!Queue._db)
|
|
89
|
-
throw new ConfigurationError(
|
|
89
|
+
throw new ConfigurationError(
|
|
90
|
+
'Queue not configured. Resolve Queue through the container first.'
|
|
91
|
+
)
|
|
90
92
|
return Queue._db
|
|
91
93
|
}
|
|
92
94
|
|
package/src/scheduler/cron.ts
CHANGED
|
@@ -17,11 +17,11 @@ export interface CronExpression {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const FIELD_RANGES: [number, number][] = [
|
|
20
|
-
[0, 59],
|
|
21
|
-
[0, 23],
|
|
22
|
-
[1, 31],
|
|
23
|
-
[1, 12],
|
|
24
|
-
[0, 6],
|
|
20
|
+
[0, 59], // minute
|
|
21
|
+
[0, 23], // hour
|
|
22
|
+
[1, 31], // day of month
|
|
23
|
+
[1, 12], // month
|
|
24
|
+
[0, 6], // day of week
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
// Parse a 5-field cron string into expanded numeric arrays.
|
|
@@ -37,7 +37,13 @@ export function parseCron(expression: string): CronExpression {
|
|
|
37
37
|
parseField(part, FIELD_RANGES[i]![0], FIELD_RANGES[i]![1])
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
minute: minute!,
|
|
42
|
+
hour: hour!,
|
|
43
|
+
dayOfMonth: dayOfMonth!,
|
|
44
|
+
month: month!,
|
|
45
|
+
dayOfWeek: dayOfWeek!,
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/**
|
|
@@ -4,13 +4,20 @@ import type { CronExpression } from './cron.ts'
|
|
|
4
4
|
export type TaskHandler = () => void | Promise<void>
|
|
5
5
|
|
|
6
6
|
const DAY_NAMES: Record<string, number> = {
|
|
7
|
-
sunday: 0,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
sunday: 0,
|
|
8
|
+
sun: 0,
|
|
9
|
+
monday: 1,
|
|
10
|
+
mon: 1,
|
|
11
|
+
tuesday: 2,
|
|
12
|
+
tue: 2,
|
|
13
|
+
wednesday: 3,
|
|
14
|
+
wed: 3,
|
|
15
|
+
thursday: 4,
|
|
16
|
+
thu: 4,
|
|
17
|
+
friday: 5,
|
|
18
|
+
fri: 5,
|
|
19
|
+
saturday: 6,
|
|
20
|
+
sat: 6,
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
/**
|
|
@@ -176,7 +183,9 @@ function dayToNumber(day: string): number {
|
|
|
176
183
|
const n = DAY_NAMES[day.toLowerCase()]
|
|
177
184
|
if (n === undefined) {
|
|
178
185
|
throw new Error(
|
|
179
|
-
`Invalid day name "${day}": expected one of ${Object.keys(DAY_NAMES)
|
|
186
|
+
`Invalid day name "${day}": expected one of ${Object.keys(DAY_NAMES)
|
|
187
|
+
.filter((_, i) => i % 2 === 0)
|
|
188
|
+
.join(', ')}`
|
|
180
189
|
)
|
|
181
190
|
}
|
|
182
191
|
return n
|
|
@@ -41,7 +41,9 @@ export default class SessionManager {
|
|
|
41
41
|
|
|
42
42
|
static get db(): Database {
|
|
43
43
|
if (!SessionManager._db) {
|
|
44
|
-
throw new ConfigurationError(
|
|
44
|
+
throw new ConfigurationError(
|
|
45
|
+
'SessionManager not configured. Resolve it through the container first.'
|
|
46
|
+
)
|
|
45
47
|
}
|
|
46
48
|
return SessionManager._db
|
|
47
49
|
}
|
|
@@ -48,7 +48,9 @@ export default class StorageManager {
|
|
|
48
48
|
|
|
49
49
|
static get driver(): StorageDriver {
|
|
50
50
|
if (!StorageManager._driver) {
|
|
51
|
-
throw new ConfigurationError(
|
|
51
|
+
throw new ConfigurationError(
|
|
52
|
+
'StorageManager not configured. Resolve it through the container first.'
|
|
53
|
+
)
|
|
52
54
|
}
|
|
53
55
|
return StorageManager._driver
|
|
54
56
|
}
|
package/src/view/compiler.ts
CHANGED
|
@@ -159,9 +159,7 @@ function compileDirective(
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
case 'islands': {
|
|
162
|
-
const src = token.args
|
|
163
|
-
? token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
164
|
-
: '/islands.js'
|
|
162
|
+
const src = token.args ? token.args.replace(/^['"]|['"]$/g, '').trim() : '/islands.js'
|
|
165
163
|
lines.push(`__out += '<script src="${escapeJs(src)}"><\\/script>';`)
|
|
166
164
|
break
|
|
167
165
|
}
|
|
@@ -25,7 +25,7 @@ export class IslandBuilder {
|
|
|
25
25
|
this.islandsDir = resolve(options.islandsDir ?? './islands')
|
|
26
26
|
this.outDir = resolve(options.outDir ?? './public')
|
|
27
27
|
this.outFile = options.outFile ?? 'islands.js'
|
|
28
|
-
this.minify = options.minify ??
|
|
28
|
+
this.minify = options.minify ?? Bun.env.NODE_ENV === 'production'
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Discover all .vue files in the islands directory. */
|
|
@@ -123,9 +123,9 @@ export class IslandBuilder {
|
|
|
123
123
|
target: 'browser',
|
|
124
124
|
plugins: [virtualEntryPlugin, vueSfcPlugin()],
|
|
125
125
|
define: {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
__VUE_OPTIONS_API__: 'true',
|
|
127
|
+
__VUE_PROD_DEVTOOLS__: 'false',
|
|
128
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
|
|
129
129
|
},
|
|
130
130
|
})
|
|
131
131
|
|
|
@@ -11,7 +11,7 @@ export function vueSfcPlugin(): BunPlugin {
|
|
|
11
11
|
return {
|
|
12
12
|
name: 'vue-sfc',
|
|
13
13
|
setup(build) {
|
|
14
|
-
build.onLoad({ filter: /\.vue$/ }, async
|
|
14
|
+
build.onLoad({ filter: /\.vue$/ }, async args => {
|
|
15
15
|
const source = await Bun.file(args.path).text()
|
|
16
16
|
const id = hashId(args.path)
|
|
17
17
|
const scopeId = `data-v-${id}`
|
|
@@ -35,11 +35,13 @@ export function vueSfcPlugin(): BunPlugin {
|
|
|
35
35
|
id,
|
|
36
36
|
inlineTemplate: !!descriptor.scriptSetup,
|
|
37
37
|
sourceMap: false,
|
|
38
|
-
templateOptions: scoped
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
templateOptions: scoped
|
|
39
|
+
? {
|
|
40
|
+
scoped: true,
|
|
41
|
+
id,
|
|
42
|
+
compilerOptions: { scopeId },
|
|
43
|
+
}
|
|
44
|
+
: undefined,
|
|
43
45
|
})
|
|
44
46
|
scriptCode = result.content
|
|
45
47
|
bindings = result.bindings
|
|
@@ -62,7 +64,7 @@ export function vueSfcPlugin(): BunPlugin {
|
|
|
62
64
|
|
|
63
65
|
if (result.errors.length > 0) {
|
|
64
66
|
throw new Error(
|
|
65
|
-
`Vue template error in ${args.path}:\n${result.errors.map(e => typeof e === 'string' ? e : e.message).join('\n')}`
|
|
67
|
+
`Vue template error in ${args.path}:\n${result.errors.map(e => (typeof e === 'string' ? e : e.message)).join('\n')}`
|
|
66
68
|
)
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -100,10 +102,7 @@ export function vueSfcPlugin(): BunPlugin {
|
|
|
100
102
|
// <script setup> with inlineTemplate — scriptCode is a complete module
|
|
101
103
|
// Rewrite the default export to capture the component and set __scopeId
|
|
102
104
|
if (scoped) {
|
|
103
|
-
output += scriptCode.replace(
|
|
104
|
-
/export\s+default\s+/,
|
|
105
|
-
'const __sfc__ = '
|
|
106
|
-
) + '\n'
|
|
105
|
+
output += scriptCode.replace(/export\s+default\s+/, 'const __sfc__ = ') + '\n'
|
|
107
106
|
output += `__sfc__.__scopeId = ${JSON.stringify(scopeId)};\n`
|
|
108
107
|
output += 'export default __sfc__;\n'
|
|
109
108
|
} else {
|
|
@@ -112,10 +111,7 @@ export function vueSfcPlugin(): BunPlugin {
|
|
|
112
111
|
} else {
|
|
113
112
|
// Options API — stitch script + template render function
|
|
114
113
|
if (scriptCode) {
|
|
115
|
-
output += scriptCode.replace(
|
|
116
|
-
/export\s+default\s*\{/,
|
|
117
|
-
'const __component__ = {'
|
|
118
|
-
) + '\n'
|
|
114
|
+
output += scriptCode.replace(/export\s+default\s*\{/, 'const __component__ = {') + '\n'
|
|
119
115
|
} else {
|
|
120
116
|
output += 'const __component__ = {};\n'
|
|
121
117
|
}
|
package/src/view/tokenizer.ts
CHANGED
|
@@ -16,7 +16,17 @@ export interface Token {
|
|
|
16
16
|
line: number
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const DIRECTIVES = new Set([
|
|
19
|
+
const DIRECTIVES = new Set([
|
|
20
|
+
'if',
|
|
21
|
+
'elseif',
|
|
22
|
+
'else',
|
|
23
|
+
'end',
|
|
24
|
+
'each',
|
|
25
|
+
'layout',
|
|
26
|
+
'block',
|
|
27
|
+
'include',
|
|
28
|
+
'islands',
|
|
29
|
+
])
|
|
20
30
|
|
|
21
31
|
export function tokenize(source: string): Token[] {
|
|
22
32
|
const tokens: Token[] = []
|