spiderly 19.8.4 → 19.8.6

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 (33) hide show
  1. package/agent/docs/angular-customization/SKILL.md +389 -0
  2. package/agent/docs/angular-customization/references/controls.generated.md +23 -0
  3. package/agent/docs/angular-customization/references/helper-functions.generated.md +39 -0
  4. package/agent/docs/angular-customization/references/ui-control-types.generated.md +24 -0
  5. package/agent/docs/angular-customization/references/validators.generated.md +13 -0
  6. package/agent/docs/authorization/SKILL.md +385 -0
  7. package/agent/docs/authorization/references/api-error-codes.generated.md +17 -0
  8. package/agent/docs/authorization/references/security-endpoints.generated.md +24 -0
  9. package/agent/docs/backend-hooks/SKILL.md +231 -0
  10. package/agent/docs/backend-localization/SKILL.md +170 -0
  11. package/agent/docs/backend-testing/SKILL.md +65 -0
  12. package/agent/docs/custom-endpoints/SKILL.md +409 -0
  13. package/agent/docs/e2e-testing/SKILL.md +139 -0
  14. package/agent/docs/entity-design/SKILL.md +346 -0
  15. package/agent/docs/entity-design/references/attributes.generated.md +53 -0
  16. package/agent/docs/file-storage/SKILL.md +262 -0
  17. package/agent/docs/filtering-patterns/SKILL.md +127 -0
  18. package/agent/docs/filtering-patterns/references/match-mode-codes.generated.md +15 -0
  19. package/agent/docs/frontend-localization/SKILL.md +120 -0
  20. package/agent/docs/mapper-customization/SKILL.md +105 -0
  21. package/agent/manifest.json +34 -0
  22. package/agent/skills/add-entity/SKILL.md +158 -0
  23. package/agent/skills/deployment/SKILL.md +551 -0
  24. package/agent/skills/ef-migrations/SKILL.md +49 -0
  25. package/agent/skills/report-gap/SKILL.md +110 -0
  26. package/agent/skills/report-gap/scripts/build-issue-url.mjs +82 -0
  27. package/agent/skills/spiderly-upgrade/SKILL.md +166 -0
  28. package/agent/skills/verify-ui/SKILL.md +148 -0
  29. package/agent/skills/verify-ui/scripts/get-admin-token.mjs +134 -0
  30. package/fesm2022/spiderly.mjs +11 -6
  31. package/fesm2022/spiderly.mjs.map +1 -1
  32. package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +29 -3
  33. package/package.json +1 -1
@@ -0,0 +1,262 @@
1
+ ---
2
+ name: file-storage
3
+ description: Configure file storage providers and blob upload behavior in Spiderly. Use when setting up storage for entity blob properties, choosing a built-in adapter, writing a custom adapter, or troubleshooting file upload issues.
4
+ ---
5
+
6
+ # File Storage
7
+
8
+ ## How storage is selected
9
+
10
+ Per blob property. Decorate a string property with a `StorageAttribute` subclass; the source generator emits upload/delete code that resolves the matching `IFileManager` adapter from DI for that property only. There is no global storage registration — every blob property declares which adapter it goes through.
11
+
12
+ ## Built-in adapters
13
+
14
+ Spiderly ships three built-in adapters and matching attributes:
15
+
16
+ | Adapter | Attribute | Best for | Returns |
17
+ | --- | --- | --- | --- |
18
+ | `DiskStorageService` | `[DiskStorage]` | Local development | File key |
19
+ | `S3PublicStorageService` | `[S3PublicStorage]` | CDN-served images, public assets | Full public URL |
20
+ | `S3PrivateStorageService` | `[S3PrivateStorage]` | Private documents, signed-URL access | S3 key |
21
+
22
+ All three implement `Spiderly.Shared.Interfaces.IFileManager`.
23
+
24
+ ## Entity property declaration
25
+
26
+ ```csharp
27
+ public class Brand : BusinessObject<int>
28
+ {
29
+ [S3PublicStorage]
30
+ [AcceptedFileTypes("image/*")]
31
+ [MaxFileSize(2_000_000)]
32
+ [StringLength(1000, MinimumLength = 1)]
33
+ public string LogoUrl { get; set; }
34
+ }
35
+
36
+ public class WarrantyRegistration : BusinessObject<long>
37
+ {
38
+ [S3PrivateStorage]
39
+ [AcceptedFileTypes("image/jpeg", "image/png", "application/pdf")]
40
+ [MaxFileSize(10_000_000)]
41
+ [StringLength(1000, MinimumLength = 1)]
42
+ public string ReceiptImageUrl { get; set; }
43
+ }
44
+ ```
45
+
46
+ `[StorageAttribute]` subclasses replace the legacy `[BlobName]` marker — presence of any subclass is what marks a string property as a blob.
47
+
48
+ | Attribute | Level | Purpose |
49
+ | --- | --- | --- |
50
+ | `[DiskStorage]` / `[S3PublicStorage]` / `[S3PrivateStorage]` | Property | Routes uploads through the matching adapter |
51
+ | `[AcceptedFileTypes("mime/type", ...)]` | Property | **Required on every blob property** — MIME-type whitelist. Build error `SPIDERLY014` if missing. No default. |
52
+ | `[MaxFileSize(N)]` | Property | Max bytes (default: 20MB) |
53
+ | `[ImageWidth(N)]` / `[ImageHeight(N)]` | Property | Validate exact image dimensions |
54
+
55
+ ## DI registration
56
+
57
+ Spiderly's source generator emits `_deps.ServiceProvider.GetRequiredService<TConcrete>()` per blob property, so each adapter you use must be discoverable by its concrete type.
58
+
59
+ `DiskStorageService` is pre-registered by the `spiderly init` template in `AppServiceExtensions.AddAppServices` (it's the dev default and has no external dependencies). When you opt in to S3, register the adapters you reference there:
60
+
61
+ ```csharp
62
+ // In your AppServiceExtensions.cs
63
+ services.AddSingleton<S3PublicStorageService>();
64
+ services.AddSingleton<S3PrivateStorageService>();
65
+ ```
66
+
67
+ The storage services are stateless wrappers around external clients (the `IAmazonS3` instance for S3, the local filesystem for Disk) — `Singleton` avoids per-resolve constructor work like the `Directory.CreateDirectory` call in `DiskStorageService`.
68
+
69
+ The S3 client itself is registered separately (one `IAmazonS3` shared by both S3 adapters):
70
+
71
+ ```csharp
72
+ services.AddSingleton<IAmazonS3>(sp =>
73
+ {
74
+ IConfiguration configuration = sp.GetRequiredService<IConfiguration>();
75
+ AmazonS3Config s3Config = new AmazonS3Config
76
+ {
77
+ ServiceURL = configuration.GetValue<string>($"{Spiderly.Shared.Settings.ConfigurationSection}:S3ServiceUrl"),
78
+ ForcePathStyle = true,
79
+ AuthenticationRegion = "auto",
80
+ };
81
+
82
+ return new AmazonS3Client(
83
+ new BasicAWSCredentials(
84
+ configuration.GetValue<string>($"{Spiderly.Shared.Settings.ConfigurationSection}:S3AccessKey"),
85
+ configuration.GetValue<string>($"{Spiderly.Shared.Settings.ConfigurationSection}:S3SecretKey")
86
+ ),
87
+ s3Config
88
+ );
89
+ });
90
+ ```
91
+
92
+ ## Configuration (`appsettings.json`)
93
+
94
+ ```json
95
+ {
96
+ "AppSettings": {
97
+ "Spiderly.Shared": {
98
+ "S3BucketName": "my-bucket",
99
+ "S3PublicEndpoint": "https://cdn.example.com"
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ S3 credentials (`S3AccessKey`, `S3SecretKey`, `S3ServiceUrl`) are app-specific settings, not in `Spiderly.Shared`.
106
+
107
+ ## Writing a custom storage adapter
108
+
109
+ Spiderly ships only the three adapters above. Other backends (Cloudinary, Azure Blob, Backblaze, on-prem MinIO, …) are written by the consumer:
110
+
111
+ 1. Implement `IFileManager`:
112
+
113
+ ```csharp
114
+ public class MyCustomStorageService : IFileManager
115
+ {
116
+ public Task<string> UploadFileAsync(...) { /* impl */ }
117
+ public Task DeleteNonActiveBlobs(...) { /* impl */ }
118
+ public Task<string> GetFileDataAsync(string key) { /* impl */ }
119
+ public Task<string> MoveBlobToEntityPathAsync(...) { /* impl */ }
120
+ public Task DeleteNonActiveEditorImages(...) { /* impl */ }
121
+ }
122
+ ```
123
+
124
+ 2. Subclass `StorageAttribute`, passing your service type to the base constructor:
125
+
126
+ ```csharp
127
+ public sealed class MyCustomStorageAttribute : StorageAttribute
128
+ {
129
+ public MyCustomStorageAttribute() : base(typeof(MyCustomStorageService)) { }
130
+ }
131
+ ```
132
+
133
+ 3. Register the service in DI:
134
+
135
+ ```csharp
136
+ services.AddTransient<MyCustomStorageService>();
137
+ ```
138
+
139
+ 4. Use the attribute on entity properties:
140
+
141
+ ```csharp
142
+ [MyCustomStorage]
143
+ [AcceptedFileTypes("image/*")]
144
+ [StringLength(1000, MinimumLength = 1)]
145
+ public string Photo { get; set; }
146
+ ```
147
+
148
+ The source generator detects custom storage attributes by the convention "attribute name ends with `Storage`" — so `MyCustomStorageAttribute` is treated as a blob marker automatically. The generator's auto-resolution of the field name is currently hard-coded for the three built-ins; for custom adapters, the generated code emits a marker comment indicating it can't dispatch — you'll need to inject your custom service directly into hand-written upload paths instead of relying on Spiderly's auto-CRUD endpoints. (Open issue: extend the source generator to read `StorageAttribute.ServiceType` from the subclass's base initializer to support fully-automatic custom-adapter routing.)
149
+
150
+ ## Upload flow
151
+
152
+ Generated methods per blob property:
153
+
154
+ ```
155
+ 1. Upload{Property}For{Entity}(IFormFile file) ← Controller endpoint
156
+ 2. → OnBefore{Property}BlobFor{Entity}UploadIsAuthorized(file, id)
157
+ 3. → OnBefore{Property}BlobFor{Entity}IsUploaded(stream, file, id)
158
+ 4. → For image/* content types:
159
+ 5. → ValidateImageFor{Property}Of{Entity}(stream, file, id)
160
+ 6. → OptimizeImageFor{Property}Of{Entity}(stream, file, id)
161
+ 7. → storageService.UploadFileAsync(...) ← resolved per [*Storage] attribute
162
+ 8. → Returns file key/URL (semantics depend on the adapter)
163
+ ```
164
+
165
+ On entity save (Update/Insert):
166
+
167
+ ```
168
+ → storageService.DeleteNonActiveBlobs(activeKey, entityName, propertyName, entityId)
169
+ ```
170
+
171
+ ## Upload hooks
172
+
173
+ Override in your entity service class:
174
+
175
+ ```csharp
176
+ // Authorization hook — run before upload
177
+ public override async Task OnBeforeMainImageBlobForProductUploadIsAuthorized(
178
+ IFormFile file, long id)
179
+ {
180
+ // Custom authorization logic
181
+ }
182
+
183
+ // Full preprocessing hook — runs for ALL file types
184
+ public override async Task<byte[]> OnBeforeMainImageBlobForProductIsUploaded(
185
+ Stream stream, IFormFile file, long id)
186
+ {
187
+ // For images: validate then optimize
188
+ if (file.ContentType.StartsWith("image/"))
189
+ {
190
+ await ValidateImageForMainImageOfProduct(stream, file, id);
191
+ stream.Position = 0;
192
+ return await OptimizeImageForMainImageOfProduct(stream, file, id);
193
+ }
194
+ return await Helper.ReadAllBytesAsync(stream);
195
+ }
196
+
197
+ // Image validation — check dimensions, format, etc.
198
+ public override async Task ValidateImageForMainImageOfProduct(
199
+ Stream stream, IFormFile file, long id)
200
+ {
201
+ await Helper.ValidateImageDimensions(stream, width: 800, height: 600);
202
+ }
203
+
204
+ // Image optimization — resize, compress, convert format
205
+ public override async Task<byte[]> OptimizeImageForMainImageOfProduct(
206
+ Stream stream, IFormFile file, long id)
207
+ {
208
+ return await Helper.OptimizeImage(stream, new Size(800, 600), quality: 80);
209
+ }
210
+ ```
211
+
212
+ ### Helper.OptimizeImage
213
+
214
+ ```csharp
215
+ public static async Task<byte[]> OptimizeImage(
216
+ Stream originalImageStream,
217
+ Size? newImageSize = null, // null = keep original size
218
+ int quality = 85 // WebP quality
219
+ )
220
+ ```
221
+
222
+ - Converts to **WebP lossy** format (via SixLabors.ImageSharp)
223
+ - Resizes with `ResizeMode.Max` (fit within bounds, not crop)
224
+ - Default quality: 85
225
+
226
+ ### Helper.ValidateImageDimensions
227
+
228
+ ```csharp
229
+ public static async Task ValidateImageDimensions(
230
+ Stream imageStream,
231
+ int width = 0, // 0 = skip width check
232
+ int height = 0 // 0 = skip height check
233
+ )
234
+ ```
235
+
236
+ Throws `SecurityViolationException` if dimensions don't match exactly.
237
+
238
+ ## Cleanup methods
239
+
240
+ ### `DeleteNonActiveBlobs`
241
+
242
+ Called automatically during entity save. Deletes all previously uploaded files for a property except the current active one. Uses file naming prefix to find old files.
243
+
244
+ ### `DeleteNonActiveEditorImages`
245
+
246
+ For rich text `[Editor]` properties paired with `[S3PublicStorage]`. Extracts `<img>` URLs from HTML, deletes uploaded images that are no longer referenced.
247
+
248
+ ```csharp
249
+ List<string> activeUrls = Helper.ExtractImageUrlsFromHtml(dto.HtmlDescription);
250
+ await _s3PublicStorageService.DeleteNonActiveEditorImages(
251
+ activeUrls, nameof(Brand), nameof(Brand.HtmlDescription) + "Image", id.ToString());
252
+ ```
253
+
254
+ Only implemented for `S3PublicStorageService`. Other built-in providers throw `NotImplementedException`.
255
+
256
+ ## File naming convention
257
+
258
+ All providers generate: `{objectId}-{objectType}-{objectProperty}-{GUID}.{extension}`
259
+
260
+ S3 providers add folder structure: `{objectType}/{objectProperty}/{objectId}/{filename}`
261
+
262
+ This prefix-based naming enables `DeleteNonActiveBlobs` to find and clean up old files without database tracking.
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: filtering-patterns
3
+ description: Customize server-side filtering, pagination, and paginated list overrides. Use when doing custom server-side filtering, overriding paginated lists, working with FilterDTO, using AdditionalFilterId for parent-child filtering, or building programmatic filters.
4
+ ---
5
+
6
+ # Filtering Patterns
7
+
8
+ ## FilterDTO Structure
9
+
10
+ ```csharp
11
+ public class FilterDTO
12
+ {
13
+ public Dictionary<string, List<FilterRuleDTO>> Filters { get; set; } = new();
14
+ public int First { get; set; } // zero-based offset
15
+ public int Rows { get; set; } // page size
16
+ public List<FilterSortMetaDTO> MultiSortMeta { get; set; } = new();
17
+ public int? AdditionalFilterIdInt { get; set; }
18
+ public long? AdditionalFilterIdLong { get; set; }
19
+ public byte? AdditionalFilterIdByte { get; set; }
20
+ }
21
+
22
+ public class FilterRuleDTO
23
+ {
24
+ public object Value { get; set; }
25
+ public string MatchMode { get; set; }
26
+ public string Operator { get; set; } // "and" / "or"
27
+ }
28
+ ```
29
+
30
+ **Match modes** — the `MatchMode` values a filter rule can use, generated from the `MatchModeCodes` contract: see [references/match-mode-codes.generated.md](references/match-mode-codes.generated.md).
31
+
32
+ Filter dictionary keys are **camelCase DTO property names**. The generated `PaginatedResultGenerator` auto-resolves them to entity paths (e.g., `categoryDisplayName` → `x.Category.Name`).
33
+
34
+ ## Override `GetPaginated{Entity}List` (Most Common)
35
+
36
+ The most common customization — pre-filter the query before pagination:
37
+
38
+ ```csharp
39
+ public override async Task<PaginatedResultDTO<CommentDTO>> GetPaginatedCommentList(
40
+ FilterDTO filterDTO,
41
+ IQueryable<Comment> query,
42
+ bool authorize)
43
+ {
44
+ // Add custom WHERE clauses
45
+ query = query.Where(x => x.IsApproved == true);
46
+
47
+ // Parent-child filtering via AdditionalFilterId
48
+ if (filterDTO.AdditionalFilterIdLong.HasValue)
49
+ query = query.Where(x => x.BlogPost.Id == filterDTO.AdditionalFilterIdLong.Value);
50
+
51
+ return await base.GetPaginatedCommentList(filterDTO, query, authorize);
52
+ }
53
+ ```
54
+
55
+ Always call `base.GetPaginated{Entity}List(...)` to keep generated filtering, sorting, pagination, and authorization intact.
56
+
57
+ ## Programmatic `FilterDTO<T>` API
58
+
59
+ Build filters in backend code with type-safe fluent API:
60
+
61
+ ```csharp
62
+ FilterDTO<Product> filter = new FilterDTO<Product>()
63
+ .AddFilter(x => x.Name, "widget", MatchModeCodes.Contains)
64
+ .AddFilter(x => x.Price, 100m, MatchModeCodes.GreaterThan)
65
+ .AddSort(x => x.CreatedAt, -1) // -1 = descending
66
+ .SetPagination(0, 25);
67
+
68
+ PaginatedResultDTO<ProductDTO> result = await GetPaginatedProductList(
69
+ filter, _context.DbSet<Product>(), authorize: false);
70
+ ```
71
+
72
+ Property names are auto-converted from PascalCase to camelCase.
73
+
74
+ ## AdditionalFilterId Pattern
75
+
76
+ Pass a parent ID from the frontend to filter child records server-side.
77
+
78
+ **Frontend (Angular):**
79
+
80
+ ```html
81
+ <spiderly-data-table
82
+ [cols]="cols"
83
+ [getPaginatedListObservableMethod]="getCommentListObservableMethod"
84
+ [additionalFilterIdLong]="blogPostId">
85
+ </spiderly-data-table>
86
+ ```
87
+
88
+ **Backend override:**
89
+
90
+ ```csharp
91
+ public override async Task<PaginatedResultDTO<CommentDTO>> GetPaginatedCommentList(
92
+ FilterDTO filterDTO,
93
+ IQueryable<Comment> query,
94
+ bool authorize)
95
+ {
96
+ if (filterDTO.AdditionalFilterIdLong.HasValue)
97
+ query = query.Where(x => x.BlogPost.Id == filterDTO.AdditionalFilterIdLong.Value);
98
+
99
+ return await base.GetPaginatedCommentList(filterDTO, query, authorize);
100
+ }
101
+ ```
102
+
103
+ Use `AdditionalFilterIdInt`, `AdditionalFilterIdLong`, or `AdditionalFilterIdByte` based on the parent entity's ID type.
104
+
105
+ ## Custom Projection (Advanced)
106
+
107
+ For fully custom DTOs (e.g., storefront-specific), call the internal overload that returns `PaginatedResult<T>` (with query + total count), then project yourself:
108
+
109
+ ```csharp
110
+ public async Task<PaginatedResultDTO<CustomDTO>> GetPaginatedProductsCustom(
111
+ FilterDTO filterDTO, IQueryable<Product> query)
112
+ {
113
+ PaginatedResult<Product> result = await GetPaginatedProductList(filterDTO, query);
114
+
115
+ List<CustomDTO> dtos = await result.Query
116
+ .Skip(filterDTO.First)
117
+ .Take(filterDTO.Rows)
118
+ .Select(x => new CustomDTO { Id = x.Id, Title = x.Title })
119
+ .ToListAsync();
120
+
121
+ return new PaginatedResultDTO<CustomDTO>
122
+ {
123
+ Data = dtos,
124
+ TotalRecords = result.TotalRecords
125
+ };
126
+ }
127
+ ```
@@ -0,0 +1,15 @@
1
+ <!-- GENERATED FROM framework-metadata.json — DO NOT EDIT.
2
+ Regenerate: `dotnet run --project Spiderly.MetadataExporter -- --out framework-metadata.json && node tools/extract-ts-metadata.mjs && node tools/gen-skill-docs.mjs` -->
3
+
4
+ # Filter match modes
5
+
6
+ String constants for the comparison operators a filter rule can use (carried on FilterRuleDTO.MatchMode). The values mirror PrimeNG's table filter match modes, so the same code drives both the Angular UI and the server-side query translation in the generated paginated-list logic.
7
+
8
+ | Name | Value | Description |
9
+ | --- | --- | --- |
10
+ | `Contains` | `contains` | String substring match, case-insensitive (value.Contains(...)). |
11
+ | `Equals` | `equals` | Equality match. For strings it is case-insensitive; for bool, number, and date/time properties it is an exact == comparison. |
12
+ | `GreaterThan` | `greaterThan` | Greater-than comparison (>), for number and date/time properties. |
13
+ | `In` | `in` | Membership match against a JSON array of values (value IN [...]), for number and id properties. |
14
+ | `LessThan` | `lessThan` | Less-than comparison (<), for number and date/time properties. |
15
+ | `StartsWith` | `startsWith` | String prefix match, case-insensitive (value.StartsWith(...)). |
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: frontend-localization
3
+ description: How Spiderly localizes Angular admin-panel UI strings — labels, buttons, menu items, validation messages, any user-facing text. Spiderly uses Transloco with flat assets/i18n/{lang}.json files loaded by SpiderlyTranslocoLoader. Use whenever you add or translate a UI string, hit a raw translation key rendering on screen, set up or change languages (provideTransloco / availableLangs), use translocoService.translate or the *transloco template directive, run i18n:extract, or wonder how form labels get auto-translated. For .NET backend strings (error messages, Excel names, IStringLocalizer), use the backend-localization skill instead.
4
+ ---
5
+
6
+ # Frontend Localization
7
+
8
+ The Spiderly Angular admin panel localizes every user-facing string through **Transloco**. Translation files are flat key→value JSON, one per language, served as static assets and loaded over HTTP at runtime — there is no compile-time embedding. Never hardcode user-facing English in a template; route it through a translation key so it localizes with the rest of the panel (e.g. a PACMS app runs entirely in Serbian).
9
+
10
+ This is a **separate system from the backend**: a string shown by the API (a `BusinessException` message) is translated on the .NET side via `IStringLocalizer` (see the **backend-localization** skill); a string rendered by the admin UI is translated here.
11
+
12
+ ## Setup (`app.config.ts`)
13
+
14
+ Transloco is registered in the app's `ApplicationConfig` with Spiderly's loader:
15
+
16
+ ```typescript
17
+ provideTransloco({
18
+ config: {
19
+ availableLangs: ['sr-Latn-RS'], // every language you ship a JSON file for
20
+ defaultLang: 'sr-Latn-RS',
21
+ reRenderOnLangChange: false, // true only if you let users switch language at runtime
22
+ },
23
+ loader: SpiderlyTranslocoLoader, // from the `spiderly` package
24
+ }),
25
+ ```
26
+
27
+ `SpiderlyTranslocoLoader` fetches `${ConfigService.frontendUrl}/assets/i18n/{lang}.json`. So:
28
+
29
+ - Files live in **`src/assets/i18n/{lang}.json`**, named **exactly** after a `lang` in `availableLangs` (e.g. `sr-Latn-RS.json`, `en.json`).
30
+ - They must be deployed as static assets (they're requested over HTTP, not bundled). They already are under `src/assets`, but a build that drops the `assets/` glob will 404 the translations and every key renders raw.
31
+
32
+ ## File format
33
+
34
+ A flat JSON object — no nesting:
35
+
36
+ ```jsonc
37
+ // src/assets/i18n/sr-Latn-RS.json
38
+ {
39
+ "Save": "Sačuvaj",
40
+ "Products": "Proizvodi",
41
+ "Product": "Proizvod",
42
+ "InvalidSKU": "SKU mora biti 6–12 velikih slova/brojeva.",
43
+ "WelcomeUser": "Dobrodošli, {{name}}!"
44
+ }
45
+ ```
46
+
47
+ Parameters use Transloco's `{{param}}` syntax: `translocoService.translate('WelcomeUser', { name: user.name })`.
48
+
49
+ ## Using translations
50
+
51
+ **In TypeScript** — inject `TranslocoService`:
52
+
53
+ ```typescript
54
+ this.translocoService.translate('Products');
55
+ this.translocoService.translate('WelcomeUser', { name: user.name });
56
+ ```
57
+
58
+ **In templates** — open a Transloco context once, then call `t(...)`:
59
+
60
+ ```html
61
+ <ng-container *transloco="let t">
62
+ <spiderly-button [label]="t('Save')"></spiderly-button>
63
+ <h2>{{ t('Products') }}</h2>
64
+ </ng-container>
65
+ ```
66
+
67
+ Add `TranslocoDirective` to the component's `imports` to use `*transloco`.
68
+
69
+ ### Adding a new key
70
+
71
+ 1. Add it to **every** `assets/i18n/{lang}.json` (one entry per language).
72
+ 2. Reference it via `translate('Key')` / `t('Key')`.
73
+
74
+ Run **`npm run i18n:extract`** (transloco-keys-manager) to scan `src/` and add any missing keys to the JSON files automatically — use it instead of hand-syncing. Note it scans only your own `src/`, **not** `node_modules/spiderly`; keys used inside Spiderly's own library components are pre-seeded by `spiderly init`, so they resolve out of the box.
75
+
76
+ ## Form label auto-translation
77
+
78
+ You rarely translate field labels by hand. When `BaseFormService` builds a form, it sets each control's `labelForDisplay` from `getTranslatedLabel(controlName)`, which normalizes the camelCase property name to a key and translates it:
79
+
80
+ - strips a trailing `Id` (`productId` → `Product`)
81
+ - strips a trailing `DisplayName` (`categoryDisplayName` → `Category`)
82
+ - upper-cases the first char, then `translate(...)`
83
+
84
+ So `name` → key `Name`, `productId` → key `Product`, `categoryDisplayName` → key `Category`. Provide those PascalCase keys in your i18n files and labels localize automatically. (The form/control mechanics themselves are documented in the **angular-customization** skill.)
85
+
86
+ ## Translating menu items and validation messages
87
+
88
+ These are ordinary `translate(...)` calls — the surrounding mechanics live in **angular-customization**; only the translation call is shown here.
89
+
90
+ ```typescript
91
+ // Menu (layout.component.ts)
92
+ menu: SpiderlyMenuItem[] = [
93
+ { label: this.translocoService.translate('Dashboard'), icon: 'pi pi-fw pi-home', routerLink: ['/dashboard'] },
94
+ ];
95
+
96
+ // Custom validator message (ValidatorAbstractService subclass)
97
+ if (value && !value.match(/^[A-Z0-9]{6,12}$/))
98
+ return { _: this.translocoService.translate('InvalidSKU') };
99
+ ```
100
+
101
+ ## Static text in custom library-style components
102
+
103
+ If you build a reusable control/component with text baked into its template, route it through Transloco rather than hardcoding — same rule the Spiderly library follows for its own controls:
104
+
105
+ ```html
106
+ <ng-container *transloco="let t">
107
+ <p-tab>{{ t('Preview') }}</p-tab>
108
+ </ng-container>
109
+ ```
110
+
111
+ ## Gotchas
112
+
113
+ - **A missing key renders the raw key.** Transloco's default `missingHandler` returns the key string, so a typo'd or unseeded key leaks the identifier (e.g. `InvalidSKU`) onto the screen instead of erroring. Treat a raw-key sighting as a missing-translation bug. Keep keys in sync across all `{lang}.json` files and lean on `i18n:extract`.
114
+ - **Filename must match `availableLangs`.** `SpiderlyTranslocoLoader` requests `{lang}.json` for the active lang; a mismatch (`sr.json` while `availableLangs` is `['sr-Latn-RS']`) 404s and the whole file falls through to raw keys.
115
+ - **Translations are fetched, not bundled.** They load over HTTP from `frontendUrl/assets/i18n/`. A misconfigured `frontendUrl` (in `ConfigService`) or a build that strips the `assets` glob breaks all translations at once — a useful first thing to check when *everything* shows raw keys.
116
+ - **`reRenderOnLangChange: false`** means a runtime language switch won't re-render already-rendered text. Set it to `true` only if you actually offer in-session language switching.
117
+
118
+ ## Backend strings are a separate system
119
+
120
+ This skill is the Angular admin panel only. Server-side strings (API error messages, Excel export names, anything from a `BusinessException`) are localized on the .NET side through `IStringLocalizer` and `Translations/{culture}.json` — a completely separate mechanism. Use the **backend-localization** skill for those.
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: mapper-customization
3
+ description: Customize Spiderly-generated Mapster mappers. Use when overriding DTO-to-entity or entity-to-DTO mapping, adding computed fields to DTOs, customizing query projections, or using the ProjectToDTO attribute.
4
+ ---
5
+
6
+ # Mapper Customization
7
+
8
+ ## Generated Methods
9
+
10
+ Spiderly generates **4 Mapster configuration methods per entity** (3 for abstract entities) in a `partial class Mapper`:
11
+
12
+ | Method | Direction | Used By |
13
+ |---|---|---|
14
+ | `{Entity}DTOToEntityConfig()` | DTO → Entity | Save flow (mapping DTO to entity before insert/update) |
15
+ | `{Entity}ToDTOConfig()` | Entity → DTO | Main UI form, general DTO mapping |
16
+ | `{Entity}ProjectToConfig()` | Entity → DTO | Paginated list queries (projection) |
17
+ | `{Entity}ExcelProjectToConfig()` | Entity → DTO | Excel export projection |
18
+
19
+ Each method returns a `TypeAdapterConfig` with Mapster mappings. For M2O relationships, the generator automatically maps `{Nav}Id` and `{Nav}DisplayName`:
20
+
21
+ ```csharp
22
+ public static TypeAdapterConfig CartToDTOConfig()
23
+ {
24
+ TypeAdapterConfig config = new();
25
+
26
+ config
27
+ .NewConfig<Cart, CartDTO>()
28
+ .Map(dest => dest.UserId, src => src.User.Id)
29
+ .Map(dest => dest.UserDisplayName, src => src.User.Email)
30
+ .Map(dest => dest.CartStatusId, src => src.CartStatus.Id)
31
+ .Map(dest => dest.CartStatusDisplayName, src => src.CartStatus.Name)
32
+ ;
33
+
34
+ return config;
35
+ }
36
+ ```
37
+
38
+ ## `[ProjectToDTO]` — Inline Custom Mappings
39
+
40
+ Add custom mappings to the projection method without overriding it. Applied to the **entity class** (not properties). `AllowMultiple = true`.
41
+
42
+ ```csharp
43
+ [ProjectToDTO(".Map(dest => dest.TransactionPrice, src => src.Transaction.Price)")]
44
+ public class Achievement : BusinessObject<long>
45
+ {
46
+ // ...
47
+ }
48
+ ```
49
+
50
+ The string is appended directly to the generated `.NewConfig<Entity, EntityDTO>()` chain. Use this for simple field mappings that the generator doesn't produce automatically.
51
+
52
+ ## Partial Method Override — Full Control
53
+
54
+ For complex mapping logic, override the entire generated method. The generator **skips generation** for any method that already exists in the user's partial `Mapper` class (detected by method name match).
55
+
56
+ **Setup** — the user's mapper file (one per project, marked with `[SpiderlyDataMapper]`):
57
+
58
+ ```csharp
59
+ using Spiderly.Shared.Attributes;
60
+
61
+ namespace MyProject.Business.DataMappers
62
+ {
63
+ [SpiderlyDataMapper]
64
+ public static partial class Mapper
65
+ {
66
+ // Override any generated method by declaring it here
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Override example:**
72
+
73
+ ```csharp
74
+ [SpiderlyDataMapper]
75
+ public static partial class Mapper
76
+ {
77
+ public static TypeAdapterConfig ProductToDTOConfig()
78
+ {
79
+ TypeAdapterConfig config = new();
80
+
81
+ config
82
+ .NewConfig<Product, ProductDTO>()
83
+ .Map(dest => dest.BrandId, src => src.Brand.Id)
84
+ .Map(dest => dest.BrandDisplayName, src => src.Brand.Name)
85
+ .Map(dest => dest.PriceWithTax, src => src.Price * 1.2m)
86
+ ;
87
+
88
+ return config;
89
+ }
90
+ }
91
+ ```
92
+
93
+ **Important:** When overriding, you take full responsibility — the generated M2O mappings won't be included automatically. Copy the generated method from `obj/.../Mapper.generated.cs` as a starting point, then add your custom mappings.
94
+
95
+ ## When to Use Each Approach
96
+
97
+ | Scenario | Approach |
98
+ |---|---|
99
+ | Add a simple computed field to projection | `[ProjectToDTO(".Map(...)")]` on entity class |
100
+ | Add multiple computed fields to projection | Stack multiple `[ProjectToDTO]` attributes |
101
+ | Complex mapping with conditionals or method calls | Override the full method in `Mapper.cs` |
102
+ | Change DTO → Entity mapping (e.g., ignore a field) | Override `{Entity}DTOToEntityConfig()` |
103
+ | Change Excel export projection | Override `{Entity}ExcelProjectToConfig()` |
104
+
105
+ Most projects never need custom mappers — the generated mappings handle M2O, M2M display names, and standard field-to-field mapping automatically.
@@ -0,0 +1,34 @@
1
+ {
2
+ "skills": [
3
+ {
4
+ "name": "add-entity",
5
+ "surface": "skill",
6
+ "description": "Scaffold a new Spiderly entity end-to-end (entity class, Angular pages, routes, menu, migration)"
7
+ },
8
+ {
9
+ "name": "deployment",
10
+ "surface": "skill",
11
+ "description": "Deploy a Spiderly project to your own infrastructure with Docker, Caddy, and Terraform. Use when deploying, redeploying, shipping, releasing, or rolling out the .NET backend or Angular admin to production — first-time setup or an ongoing deploy — and when diagnosing a down or erroring production origin (502/521, container crash-loop, failed deploy workflow). Also covers CI/CD pipelines, TLS with Cloudflare origin certificates, database backups and restores, and infrastructure-as-code layout for a Spiderly app."
12
+ },
13
+ {
14
+ "name": "ef-migrations",
15
+ "surface": "skill",
16
+ "description": "Create and apply EF Core migrations in Spiderly projects. Use when adding, modifying, or removing entity properties, changing column types, renaming columns, or any database schema change that requires a migration."
17
+ },
18
+ {
19
+ "name": "report-gap",
20
+ "surface": "skill",
21
+ "description": "Use the moment you're forced into a workaround because the clean Spiderly-native path didn't exist — you looked for a lifecycle hook, an override, an entity attribute, or a generator option and it was structurally missing, so you had to bypass or copy generated code, or reach into framework internals. Also use when the gap is in Spiderly's own skills, plugins, or docs — a skill that should have fired but didn't, or skill/doc content that was missing, stale, or wrong for the case you hit. Turns that gap into a pre-filled GitHub issue URL against filiptrivan/spiderly that the user copies and submits. Also invoke manually to report a Spiderly limitation. NOT for hacks in the consumer's own business logic, NOT for ordinary bugs in your own code."
22
+ },
23
+ {
24
+ "name": "spiderly-upgrade",
25
+ "surface": "skill",
26
+ "description": "Upgrade a Spiderly consumer app's package version (NuGet + npm) to a newer Spiderly release. Use when the user asks to upgrade Spiderly, bump the Spiderly version, jump to a newer Spiderly release, or migrate to a newer Spiderly package version. Do NOT use for EF Core schema migrations (see ef-migrations skill)."
27
+ },
28
+ {
29
+ "name": "verify-ui",
30
+ "surface": "skill",
31
+ "description": "Log past Spiderly's email-code auth wall to visually verify the running Angular admin panel — screenshot a page, click through a flow, or dogfood a UI change without writing a test. Use when asked to \"verify the admin UI\", \"check this page renders\", \"screenshot the admin panel\", \"does this screen look right\", or to eyeball a change in the live app. For authoring Playwright test suites or debugging CI traces, use the e2e-testing skill instead."
32
+ }
33
+ ]
34
+ }