@appweaver/cli 1.0.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.
Files changed (76) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +7 -0
  3. package/build/build-command.d.ts +2 -0
  4. package/build/build-command.js +15 -0
  5. package/build/build-project.d.ts +8 -0
  6. package/build/build-project.js +19 -0
  7. package/build/index.d.ts +2 -0
  8. package/build/index.js +18 -0
  9. package/generate/generate-command.d.ts +2 -0
  10. package/generate/generate-command.js +38 -0
  11. package/generate/generate-schema.d.ts +12 -0
  12. package/generate/generate-schema.js +475 -0
  13. package/generate/generate-types.d.ts +10 -0
  14. package/generate/generate-types.js +86 -0
  15. package/generate/index.d.ts +3 -0
  16. package/generate/index.js +19 -0
  17. package/migrate/index.d.ts +1 -0
  18. package/migrate/index.js +17 -0
  19. package/migrate/migrate-command.d.ts +2 -0
  20. package/migrate/migrate-command.js +13 -0
  21. package/migration/index.d.ts +1 -0
  22. package/migration/index.js +17 -0
  23. package/migration/migration-command.d.ts +2 -0
  24. package/migration/migration-command.js +34 -0
  25. package/openapi/index.d.ts +1 -0
  26. package/openapi/index.js +17 -0
  27. package/openapi/openapi-command.d.ts +2 -0
  28. package/openapi/openapi-command.js +46 -0
  29. package/package.json +56 -0
  30. package/seed/index.d.ts +1 -0
  31. package/seed/index.js +17 -0
  32. package/seed/seed-command.d.ts +2 -0
  33. package/seed/seed-command.js +33 -0
  34. package/skill/GUIDELINES.md +298 -0
  35. package/skill/SKILL.md +593 -0
  36. package/skill/references/cache.md +207 -0
  37. package/skill/references/cli.md +213 -0
  38. package/skill/references/client.md +507 -0
  39. package/skill/references/configuration.md +402 -0
  40. package/skill/references/database.md +134 -0
  41. package/skill/references/dependency-injection.md +214 -0
  42. package/skill/references/events.md +152 -0
  43. package/skill/references/mailer.md +235 -0
  44. package/skill/references/queue.md +196 -0
  45. package/skill/references/resources.md +961 -0
  46. package/skill/references/scheduler.md +184 -0
  47. package/skill/references/security.md +694 -0
  48. package/skill/references/storage.md +251 -0
  49. package/start/index.d.ts +2 -0
  50. package/start/index.js +18 -0
  51. package/start/start-command.d.ts +2 -0
  52. package/start/start-command.js +17 -0
  53. package/start/start-project.d.ts +8 -0
  54. package/start/start-project.js +147 -0
  55. package/testing/index.d.ts +1 -0
  56. package/testing/index.js +17 -0
  57. package/testing/testing-command.d.ts +2 -0
  58. package/testing/testing-command.js +96 -0
  59. package/update/index.d.ts +2 -0
  60. package/update/index.js +18 -0
  61. package/update/update-command.d.ts +2 -0
  62. package/update/update-command.js +84 -0
  63. package/update/update-packages.d.ts +10 -0
  64. package/update/update-packages.js +45 -0
  65. package/update/update-skill.d.ts +8 -0
  66. package/update/update-skill.js +93 -0
  67. package/utils/index.d.ts +3 -0
  68. package/utils/index.js +19 -0
  69. package/utils/loader-util.d.ts +29 -0
  70. package/utils/loader-util.js +132 -0
  71. package/utils/path-util.d.ts +41 -0
  72. package/utils/path-util.js +98 -0
  73. package/utils/process-util.d.ts +39 -0
  74. package/utils/process-util.js +92 -0
  75. package/weaver.d.ts +2 -0
  76. package/weaver.js +53 -0
@@ -0,0 +1,214 @@
1
+ # Dependency Injection
2
+
3
+ The application context provides a lightweight dependency injection (DI) system through `define` and `inject`. Values,
4
+ class constructors, and singleton instances are stored in the context and resolved by name or class reference.
5
+
6
+ #### `define(value, nameOrClass?, mode?)`
7
+
8
+ Registers a value or class constructor in the application context.
9
+
10
+ | Parameter | Type | Description |
11
+ |---------------|------------------------------------------------|-------------------------------------------------------------------------------------------------------|
12
+ | `value` | any | The value, instance, or class constructor to register |
13
+ | `nameOrClass` | `string \| symbol \| Class \| Function` | Token used to look up this definition. Defaults to the value's class name or `RESOURCE_NAME` property |
14
+ | `mode` | `'ignore' \| 'override' \| 'append' \| 'fail'` | How to handle duplicates. Defaults to `'ignore'` |
15
+
16
+ **Register a plain value with a string token:**
17
+
18
+ ```ts
19
+ import { define } from '@appweaver/core';
20
+
21
+ define('https://api.example.com', 'ApiBaseUrl');
22
+ ```
23
+
24
+ **Register a class instance under an abstract class token:**
25
+
26
+ ```ts
27
+ import { define } from '@appweaver/core';
28
+ import { SecurityStore } from '@appweaver/common';
29
+ import { RedisSecurityStore } from './redis-security-store';
30
+
31
+ // RedisSecurityStore will be looked up by SecurityStore.name
32
+ define(new RedisSecurityStore(), SecurityStore);
33
+ ```
34
+
35
+ **Register a class constructor (lazy instantiation on the first injection):**
36
+
37
+ ```ts
38
+ import { define } from '@appweaver/core';
39
+ import { Cache } from '@appweaver/common';
40
+ import { RedisCacheService } from './redis-cache-service';
41
+
42
+ // RedisCacheService is instantiated the first time inject(Cache) is called
43
+ define(RedisCacheService, Cache);
44
+ ```
45
+
46
+ **Register with a symbol token:**
47
+
48
+ ```ts
49
+ import { define } from '@appweaver/core';
50
+
51
+ const RATE_LIMITER = Symbol('RateLimiter');
52
+ define(new TokenBucketLimiter(), RATE_LIMITER);
53
+ ```
54
+
55
+ **Override an existing definition:**
56
+
57
+ ```ts
58
+ import { define } from '@appweaver/core';
59
+ import { Cache } from '@appweaver/common';
60
+
61
+ define(new InMemoryCacheService(), Cache, 'override');
62
+ ```
63
+
64
+ #### `inject(nameOrClass, required?)`
65
+
66
+ Retrieves a definition from the application context. If the stored value is a class constructor, it is instantiated on
67
+ first access, and the instance replaces the constructor in the context (singleton pattern).
68
+
69
+ | Parameter | Type | Description |
70
+ |---------------|-----------------------------------------|----------------------------------------|
71
+ | `nameOrClass` | `string \| symbol \| Class \| Function` | Token that identifies the definition |
72
+ | `required` | `boolean` | Throw if not found. Defaults to `true` |
73
+
74
+ **Inject by abstract class (the most common pattern):**
75
+
76
+ ```ts
77
+ import { inject } from '@appweaver/core';
78
+ import { SecurityStore } from '@appweaver/common';
79
+
80
+ export class AuthService {
81
+ private readonly _store = inject(SecurityStore);
82
+ }
83
+ ```
84
+
85
+ **Inject by string token:**
86
+
87
+ ```ts
88
+ import { inject } from '@appweaver/core';
89
+
90
+ const apiUrl = inject<string>('ApiBaseUrl');
91
+ ```
92
+
93
+ **Inject by symbol token:**
94
+
95
+ ```ts
96
+ import { inject } from '@appweaver/core';
97
+
98
+ const RATE_LIMITER = Symbol('RateLimiter');
99
+ const limiter = inject<RateLimiter>(RATE_LIMITER);
100
+ ```
101
+
102
+ **Optionally inject (returns `undefined` instead of throwing):**
103
+
104
+ ```ts
105
+ import { inject } from '@appweaver/core';
106
+ import { Mailer } from '@appweaver/common';
107
+
108
+ const mailer = inject(Mailer, false); // undefined if not registered
109
+ if (mailer) {
110
+ await mailer.send(message);
111
+ }
112
+ ```
113
+
114
+ **Real-world example — service using injected dependencies:**
115
+
116
+ ```ts
117
+ // src/features/notifications/notification-service.ts
118
+ import { inject } from '@appweaver/core';
119
+ import { Mailer, Cache } from '@appweaver/common';
120
+ import { CacheService } from '@appweaver/core';
121
+
122
+ export class NotificationService {
123
+ private readonly _mailer = inject(Mailer);
124
+ private readonly _cache = inject(CacheService);
125
+
126
+ async notify(userId: number, message: string) {
127
+ const key = `notification:${userId}`;
128
+ if (await this._cache.getCachedValue(key)) return;
129
+ await this._mailer.send({ to: userId, body: message });
130
+ await this._cache.addToCache(key, true, 60_000);
131
+ }
132
+ }
133
+ ```
134
+
135
+ Then register it so other code can inject it:
136
+
137
+ ```ts
138
+ import { define } from '@appweaver/core';
139
+ import { NotificationService } from './notification-service';
140
+
141
+ define(NotificationService);
142
+ // or define the instance directly:
143
+ define(new NotificationService(), NotificationService);
144
+ ```
145
+
146
+ #### `loadProvider(baseDir, classPath, definition?, required?)`
147
+
148
+ Dynamically loads a class from a file path and registers it in the context. The first exported constructor in the
149
+ resolved module is used. This is the standard way to load infrastructure provider implementations that are configured
150
+ via environment-specific paths.
151
+
152
+ | Parameter | Type | Description |
153
+ |--------------|-----------|--------------------------------------------------------------------------------------|
154
+ | `baseDir` | `string` | Base directory for resolving relative paths |
155
+ | `classPath` | `string` | Path to the module — relative to `baseDir`, project source path, or npm package name |
156
+ | `definition` | `Class` | Abstract class / token to register the loaded class under |
157
+ | `required` | `boolean` | Throw on load failure. Defaults to `true` |
158
+
159
+ **Load a provider and register it under an abstract token:**
160
+
161
+ ```ts
162
+ import { loadProvider } from '@appweaver/core';
163
+ import { Database } from '@appweaver/common';
164
+
165
+ // Loads the first exported class from './providers/postgres-database.ts'
166
+ // and registers it as Database
167
+ loadProvider(__dirname, './providers/postgres-database', Database);
168
+ ```
169
+
170
+ **Load an optional provider (no error if the file is missing):**
171
+
172
+ ```ts
173
+ import { loadProvider } from '@appweaver/core';
174
+ import { Queue } from '@appweaver/common';
175
+
176
+ loadProvider(__dirname, config.QUEUE_PROVIDER, Queue, false);
177
+ ```
178
+
179
+ **Load a provider from an NPM package:**
180
+
181
+ ```ts
182
+ import { loadProvider } from '@appweaver/core';
183
+ import { Cache } from '@appweaver/common';
184
+
185
+ loadProvider(__dirname, '@myorg/redis-cache', Cache);
186
+ ```
187
+
188
+ **Loading multiple providers at startup (typical `main.ts` pattern):**
189
+
190
+ ```ts
191
+ // src/main.ts
192
+ import { loadProvider } from '@appweaver/core';
193
+ import { Database, Cache, Mailer } from '@appweaver/common';
194
+ import { config } from '@appweaver/common';
195
+
196
+ // Required infrastructure
197
+ loadProvider(__dirname, config.DATABASE_PROVIDER, Database);
198
+ loadProvider(__dirname, config.CACHE_PROVIDER, Cache);
199
+
200
+ // Optional infrastructure
201
+ loadProvider(__dirname, config.MAILER_PROVIDER, Mailer, false);
202
+
203
+ // Internal feature service (no token — registered by its own class name)
204
+ loadProvider(__dirname, '../notifications/notification-service', undefined, false);
205
+ ```
206
+
207
+ After `loadProvider` runs, the loaded class is available via `inject`:
208
+
209
+ ```ts
210
+ import { inject } from '@appweaver/core';
211
+ import { Database } from '@appweaver/common';
212
+
213
+ const db = inject(Database); // resolves the class loaded from config.DATABASE_PROVIDER
214
+ ```
@@ -0,0 +1,152 @@
1
+ # Events
2
+
3
+ The events module provides a resource-scoped publish/subscribe system built on top of Node.js `EventEmitter`. It is used
4
+ internally by the framework to broadcast resource lifecycle changes (`create`, `update`, `delete`, `get`, `list`) and
5
+ can be used directly to react to those events anywhere in the application.
6
+
7
+ ## Injecting Events
8
+
9
+ ```ts
10
+ import { inject } from '@appweaver/core';
11
+ import { Events } from '@appweaver/common';
12
+
13
+ const events = inject(Events);
14
+ ```
15
+
16
+ ---
17
+
18
+ #### `events.onResourceEvent<T>(resourceName, event, listener)`
19
+
20
+ Registers a listener for a specific resource and action. Returns a listener ID that can be used to unsubscribe.
21
+
22
+ | Parameter | Type | Description |
23
+ |----------------|--------------------|-------------------------------------------------------|
24
+ | `resourceName` | `string` | The name of the resource model (e.g. `'Product'`) |
25
+ | `event` | `ActionType` | `'create'`, `'update'`, `'delete'`, `'get'`, `'list'` |
26
+ | `listener` | `EventListener<T>` | Callback receiving `EventData<T>` |
27
+
28
+ `EventData<T>` shape:
29
+
30
+ ```ts
31
+ type EventData<T> = {
32
+ previous?: T; // present on 'update' and 'delete'
33
+ current: T;
34
+ };
35
+ ```
36
+
37
+ ```ts
38
+ const listenerId = events.onResourceEvent<Product>('Product', 'create', ({ current }) => {
39
+ logger.info('New product created:', current.name);
40
+ });
41
+ ```
42
+
43
+ **Listening to updates** (access both previous and current state):
44
+
45
+ ```ts
46
+ events.onResourceEvent<User>('User', 'update', ({ previous, current }) => {
47
+ if (previous?.email !== current.email) {
48
+ sendEmailChangeNotification(current);
49
+ }
50
+ });
51
+ ```
52
+
53
+ ---
54
+
55
+ #### `events.emitResourceEvent<T>(resourceName, event, data)`
56
+
57
+ Emits an event for a resource. The framework calls this automatically after every resource mutation; use it directly
58
+ only when you need to emit custom events from your own service logic.
59
+
60
+ | Parameter | Type | Description |
61
+ |----------------|----------------|-------------------------------------------------|
62
+ | `resourceName` | `string` | Resource model name |
63
+ | `event` | `ActionType` | Action type |
64
+ | `data` | `EventData<T>` | Object with `current` (and optional `previous`) |
65
+
66
+ ```ts
67
+ events.emitResourceEvent<Order>('Order', 'create', { current: newOrder });
68
+ ```
69
+
70
+ ---
71
+
72
+ #### `events.removeResourceEvent(listenerId)`
73
+
74
+ Unregisters a listener by the ID returned from `onResourceEvent`. Returns `true` if the listener was found and removed.
75
+
76
+ ```ts
77
+ const listenerId = events.onResourceEvent('Product', 'delete', handler);
78
+ // ... later:
79
+ events.removeResourceEvent(listenerId);
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Configuration
85
+
86
+ | Key | Type | Default | Description |
87
+ |------------------------|----------|----------------------------------------|---------------------------------------|
88
+ | `EVENTS_MAX_LISTENERS` | `int` | `10` | Max listeners per event (Node.js cap) |
89
+ | `EVENTS_PROVIDER` | `string` | `'@appweaver/core/events/node-events'` | Path to the Events implementation |
90
+
91
+ ## Real-world example
92
+
93
+ Register listeners at startup, then let the framework drive the emit side:
94
+
95
+ ```ts
96
+ // src/features/notifications/notification-listener.ts
97
+ import { inject } from '@appweaver/core';
98
+ import { Events, logger, Mailer } from '@appweaver/common';
99
+
100
+ export function registerNotificationListeners() {
101
+ const events = inject(Events);
102
+ const mailer = inject(Mailer, false);
103
+
104
+ events.onResourceEvent<Order>('Order', 'create', async ({ current }) => {
105
+ if (!mailer) return;
106
+ await mailer.sendEmail({
107
+ to: current.customerEmail,
108
+ subject: 'Order received',
109
+ text: `Your order #${current.id} has been placed.`
110
+ });
111
+ });
112
+
113
+ events.onResourceEvent<User>('User', 'delete', ({ current }) => {
114
+ logger.info(`User ${current.id} was deleted — cleaning up sessions`);
115
+ cleanupSessions(current.id);
116
+ });
117
+ }
118
+ ```
119
+
120
+ Call `registerNotificationListeners()` during application startup, after the providers are loaded.
121
+
122
+ ---
123
+
124
+ ## Using Node.js EventEmitter directly
125
+
126
+ Since `Events` extends Node.js `EventEmitter`, you can use the standard `.on()` / `.emit()` API for custom application
127
+ events alongside the resource event helpers.
128
+
129
+ ```ts
130
+ import { inject } from '@appweaver/core';
131
+ import { Events } from '@appweaver/common';
132
+
133
+ const events = inject(Events);
134
+
135
+ // Listen for a custom event
136
+ events.on('user-registered', (user: User) => {
137
+ logger.info(`Welcome email queued for ${user.email}`);
138
+ mailer.sendWelcomeEmail(user);
139
+ });
140
+
141
+ // Emit the event from your service
142
+ events.emit('user-registered', newUser);
143
+ ```
144
+
145
+ You can use any string as the event name. The listener receives whatever arguments you pass to `emit`. Use
146
+ `events.once()` if you only need to handle the event one time:
147
+
148
+ ```ts
149
+ events.once('user-registered', (user: User) => {
150
+ logger.info(`First registration ever: ${user.email}`);
151
+ });
152
+ ```
@@ -0,0 +1,235 @@
1
+ # Mailer
2
+
3
+ The mailer module sends transactional emails. The default implementation (`SmtpMailer`) uses Nodemailer over SMTP. A
4
+ `JsonMailer` is available for development and tests — it returns a JSON representation of the email instead of sending
5
+ it.
6
+
7
+ ## Injecting Mailer
8
+
9
+ Mailer is an optional infrastructure. Inject it with `required: false` to avoid errors when no mailer is configured.
10
+
11
+ ```ts
12
+ import { inject } from '@appweaver/core';
13
+ import { Mailer } from '@appweaver/common';
14
+
15
+ const mailer = inject(Mailer, false); // undefined if not registered
16
+ ```
17
+
18
+ ---
19
+
20
+ #### `mailer.sendEmail(data)`
21
+
22
+ Sends an email. Returns `true` on success.
23
+
24
+ | Field | Type | Required | Description |
25
+ |---------------|----------------|----------|-------------------------------------------|
26
+ | `to` | `string` | Yes | Recipient address |
27
+ | `subject` | `string` | Yes | Email subject line |
28
+ | `text` | `string` | Yes | Plain-text body |
29
+ | `html` | `string` | No | HTML body (falls back to wrapping `text`) |
30
+ | `attachments` | `Attachment[]` | No | Provider-specific attachment objects |
31
+
32
+ ```ts
33
+ await mailer.sendEmail({
34
+ to: 'alice@example.com',
35
+ subject: 'Welcome!',
36
+ text: 'Thanks for signing up.',
37
+ html: '<p>Thanks for <strong>signing up</strong>.</p>'
38
+ });
39
+ ```
40
+
41
+ **With attachments** (Nodemailer format for SmtpMailer):
42
+
43
+ ```ts
44
+ await mailer.sendEmail({
45
+ to: 'bob@example.com',
46
+ subject: 'Your invoice',
47
+ text: 'Please find your invoice attached.',
48
+ attachments: [
49
+ { filename: 'invoice.pdf', path: '/tmp/invoice.pdf' }
50
+ ]
51
+ });
52
+ ```
53
+
54
+ ---
55
+
56
+ #### `mailer.checkHealth()`
57
+
58
+ Verifies that the mail transport is reachable (`transporter.verify()` for SMTP). Returns a `HealthCheckResult`.
59
+
60
+ ---
61
+
62
+ ## Configuration
63
+
64
+ | Key | Type | Default | Description |
65
+ |-------------------------|----------|----------------------------------------|-----------------------------------|
66
+ | `MAILER_PROVIDER` | `string` | `'@appweaver/core/mailer/smtp-mailer'` | Path to the Mailer implementation |
67
+ | `MAILER_SENDER_NAME` | `string` | — | Display name of the sender |
68
+ | `MAILER_SENDER_ADDRESS` | `string` | — | From address |
69
+ | `MAILER_SMTP_HOST` | `string` | `'127.0.0.1'` | SMTP server hostname |
70
+ | `MAILER_SMTP_PORT` | `int` | `587` | SMTP server port |
71
+ | `MAILER_SMTP_SECURE` | `bool` | `false` | Use TLS (`true` for port 465) |
72
+ | `MAILER_SMTP_USER` | `string` | — | SMTP authentication username |
73
+ | `MAILER_SMTP_PASSWORD` | `string` | — | SMTP authentication password |
74
+
75
+ **`appweaver.json` example:**
76
+
77
+ ```json
78
+ {
79
+ "MAILER_SENDER_NAME": "My App",
80
+ "MAILER_SENDER_ADDRESS": "no-reply@myapp.com",
81
+ "MAILER_SMTP_HOST": "smtp.mailgun.org",
82
+ "MAILER_SMTP_PORT": 587,
83
+ "MAILER_SMTP_USER": "postmaster@mg.myapp.com",
84
+ "MAILER_SMTP_PASSWORD": "secret"
85
+ }
86
+ ```
87
+
88
+ **Use `JsonMailer` in development or tests:**
89
+
90
+ ```json
91
+ {
92
+ "MAILER_PROVIDER": "@appweaver/core/mailer/json-mailer"
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Real-world example
99
+
100
+ ```ts
101
+ import { inject } from '@appweaver/core';
102
+ import { Mailer } from '@appweaver/common';
103
+
104
+ export class AuthService {
105
+ private readonly _mailer = inject(Mailer, false);
106
+
107
+ async sendPasswordReset(email: string, token: string): Promise<void> {
108
+ if (!this._mailer) return; // mailer not configured, skip silently
109
+
110
+ await this._mailer.sendEmail({
111
+ to: email,
112
+ subject: 'Reset your password',
113
+ text: `Use this token to reset your password: ${token}`,
114
+ html: `<p>Use this token to reset your password: <strong>${token}</strong></p>`
115
+ });
116
+ }
117
+ }
118
+ ```
119
+
120
+ ---
121
+
122
+ ## EmailService
123
+
124
+ `EmailService` is a higher-level wrapper around `Mailer` that routes all emails through the `Queue` infrastructure. This
125
+ decouples the caller from the SMTP transport, provides retry semantics, and avoids blocking the request thread on email
126
+ delivery.
127
+
128
+ Inject it like any other service — no `required: false` needed since it is always registered when the mailer module is
129
+ loaded.
130
+
131
+ ```ts
132
+ import { EmailService, inject } from '@appweaver/core';
133
+ ```
134
+
135
+ ---
136
+
137
+ #### `emailService.sendEmail(email)`
138
+
139
+ Fire-and-forget. Adds the email to the queue and returns immediately — delivery happens asynchronously.
140
+
141
+ ```ts
142
+ await emailService.sendEmail({
143
+ to: 'alice@example.com',
144
+ subject: 'Welcome!',
145
+ text: 'Thanks for signing up.',
146
+ html: '<p>Thanks for <strong>signing up</strong>.</p>'
147
+ });
148
+ ```
149
+
150
+ ---
151
+
152
+ #### `emailService.sendEmailAndWait(email)`
153
+
154
+ Queues the email and resolves only once delivery succeeds, or rejects if it fails. Use this when you need a delivery
155
+ confirmation before proceeding (e.g., before returning a 200 to the client).
156
+
157
+ Returns `true` on success.
158
+
159
+ ```ts
160
+ const sent = await emailService.sendEmailAndWait({
161
+ to: 'bob@example.com',
162
+ subject: 'Your invoice',
163
+ text: 'Please find your invoice attached.',
164
+ attachments: [{ filename: 'invoice.pdf', path: '/tmp/invoice.pdf' }]
165
+ });
166
+ ```
167
+
168
+ ---
169
+
170
+ #### `emailService.sendEmailBulk(emails)`
171
+
172
+ Queues all emails at once without waiting for any to finish. Prefer this when sending a large batch and delivery
173
+ confirmation is not required.
174
+
175
+ ```ts
176
+ await emailService.sendEmailBulk(
177
+ users.map((u) => ({
178
+ to: u.email,
179
+ subject: 'New feature announcement',
180
+ text: `Hi ${u.name}, we just shipped something new...`
181
+ }))
182
+ );
183
+ ```
184
+
185
+ ---
186
+
187
+ #### `emailService.sendEmailBulkAndWait(emails)`
188
+
189
+ Queues all emails and waits for every delivery attempt to settle. Returns an array of
190
+ `{ success: boolean; error?: Error }` in the same order as the input — failed deliveries do not reject the whole call.
191
+
192
+ ```ts
193
+ const results = await emailService.sendEmailBulkAndWait(emails);
194
+
195
+ for (const [i, result] of results.entries()) {
196
+ if (!result.success) {
197
+ logger.error(result.error, `Failed to deliver email to ${emails[i].to}`);
198
+ }
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ### Real-world example — post-registration flow
205
+
206
+ ```ts
207
+ import { EmailService, inject } from '@appweaver/core';
208
+
209
+ export class RegistrationService {
210
+ private readonly _emailService = inject(EmailService);
211
+
212
+ async register(user: { email: string; name: string }): Promise<void> {
213
+ // ... persist user to DB ...
214
+
215
+ // Send welcome email and wait for confirmation before returning
216
+ await this._emailService.sendEmailAndWait({
217
+ to: user.email,
218
+ subject: 'Welcome to MyApp!',
219
+ text: `Hi ${user.name}, your account is ready.`,
220
+ html: `<p>Hi <strong>${user.name}</strong>, your account is ready.</p>`
221
+ });
222
+ }
223
+
224
+ async notifyAdmins(admins: { email: string }[], message: string): Promise<void> {
225
+ // Blast all admin notifications without blocking
226
+ await this._emailService.sendEmailBulk(
227
+ admins.map((a) => ({
228
+ to: a.email,
229
+ subject: 'Admin notification',
230
+ text: message
231
+ }))
232
+ );
233
+ }
234
+ }
235
+ ```