includio-cms 0.37.3 → 0.37.4

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 (38) hide show
  1. package/API.md +3 -2
  2. package/CHANGELOG.md +19 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +21 -1
  5. package/dist/core/server/entries/operations/resolveEntry.d.ts +4 -2
  6. package/dist/core/server/entries/operations/resolveEntry.js +24 -42
  7. package/dist/core/server/entries/operations/versionResolve.d.ts +39 -0
  8. package/dist/core/server/entries/operations/versionResolve.js +52 -0
  9. package/dist/core/server/fields/populateSelect.d.ts +22 -0
  10. package/dist/core/server/fields/populateSelect.js +28 -0
  11. package/dist/core/server/fields/resolveRelationFields.d.ts +8 -8
  12. package/dist/core/server/fields/resolveRelationFields.js +135 -113
  13. package/dist/db-postgres/index.js +48 -0
  14. package/dist/db-postgres/schema/entry.js +2 -2
  15. package/dist/db-postgres/schema/entryVersion.js +5 -2
  16. package/dist/db-postgres/schema/mediaFileTag.js +4 -2
  17. package/dist/paraglide/messages/_index.d.ts +36 -3
  18. package/dist/paraglide/messages/_index.js +71 -3
  19. package/dist/paraglide/messages/en.d.ts +5 -0
  20. package/dist/paraglide/messages/en.js +14 -0
  21. package/dist/paraglide/messages/pl.d.ts +5 -0
  22. package/dist/paraglide/messages/pl.js +14 -0
  23. package/dist/types/adapters/db.d.ts +13 -1
  24. package/dist/types/cms.schema.d.ts +12 -0
  25. package/dist/types/cms.schema.js +8 -2
  26. package/dist/types/collections.d.ts +3 -0
  27. package/dist/types/entries.d.ts +2 -0
  28. package/dist/types/fields.d.ts +12 -0
  29. package/dist/updates/0.37.4/index.d.ts +2 -0
  30. package/dist/updates/0.37.4/index.js +17 -0
  31. package/dist/updates/index.js +3 -1
  32. package/package.json +1 -1
  33. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  34. package/dist/paraglide/messages/hello_world.js +0 -33
  35. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  36. package/dist/paraglide/messages/login_hello.js +0 -34
  37. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  38. package/dist/paraglide/messages/login_please_login.js +0 -34
package/API.md CHANGED
@@ -1,8 +1,8 @@
1
- # includio-cms — Public API v0.37.3
1
+ # includio-cms — Public API v0.37.4
2
2
 
3
3
  > Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
4
4
 
5
- Entry points: **23** · Stable: **715** · Experimental: **4**
5
+ Entry points: **23** · Stable: **716** · Experimental: **4**
6
6
 
7
7
  Tags:
8
8
  - `@public` — frozen for v1.0; semver-protected.
@@ -152,6 +152,7 @@ Tags:
152
152
  - `interface RadioField`
153
153
  - `interface RelationField`
154
154
  - `type RelationFieldData = string | string[]`
155
+ - `interface RelationPopulate` — Chuda populacja relacji: allowlist populowanych pól + cap głębokości.
155
156
  - `interface SCFigureAttrs`
156
157
  - `interface SCInlineBlockAttrs`
157
158
  - `interface SCMark`
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.37.4 — 2026-07-20
7
+
8
+ Wydajność populacji: chude relacje/listy (opt-in `select`/`populate`) + mniejszy over-fetch wersji + indeksy ścieżki treści. Wyszło z diagnozy wolnych podstron u pod-zwrotnikami (bloki-listy ciągnące pełne media/relacje).
9
+
10
+ ### Added
11
+ - **Chuda populacja relacji (`select`/`depth`) — trzy warstwy.** Na polu relacji `populate: { select: ["title", "hero"], depth: 0 }`, na kolekcji docelowej `defaultPopulate`, oraz per-call `resolveEntry(s)({ populate: { select, fields } })`. Precedencja: call > pole > kolekcja > pełna populacja (domyślnie, wstecznie zgodnie). `select` tnie także pola **media** — galeria nie jest już populowana, gdy widok czyta tylko `heroImage`. Meta (`_id/_slug/_url/...`) zawsze obecne.
12
+ - **`resolveEntries({ populate: { select: [...] } })` dla list/bloków.** Chudy widok listy bez ciągnięcia pełnego drzewa każdego wpisu (media + relacje).
13
+
14
+ ### Fixed
15
+ - **Mniejszy over-fetch wersji.** Wybór wersji czyta teraz tylko metadane (`getEntryVersionsMeta`, bez kolumny `data`), a pełne dane hydratują się wyłącznie dla wybranej wersji (`getEntryVersionData`). Wcześniej każda relacja/wpis ciągnął pełny JSON wszystkich swoich wersji i wybierał najnowszą w JS — koszt mnożony przez głębokość i liczbę wpisów.
16
+ - **Indeksy ścieżki treści.** `entry(slug)`, `entry(type)`, `entry_version(entry_id, lang, version_number)`, `entry_version(entry_id, published_at)`, `media_file_tag(tag_id)`. Aplikowane przy `db:push`.
17
+
18
+ ### Breaking
19
+ - **Kontrakt `DatabaseAdapter`: nowe wymagane metody `getEntryVersionsMeta` + `getEntryVersionData`.** Dotyczy wyłącznie własnych implementacji adaptera DB (oficjalny `db-postgres` zaktualizowany). `getEntryVersions` (pełne wersje) pozostaje bez zmian.
20
+
21
+ ### Notes
22
+
23
+ Indeksy z tego wydania wchodzą przy najbliższym `pnpm db:push` w projekcie klienta — brak osobnej migracji SQL.
24
+
6
25
  ## 0.37.2 — 2026-07-16
7
26
 
8
27
  Powierzchnia SEO/AEO rdzenia — reużywalne helpery, które raz naprawione w rdzeniu działają u każdego klienta (wyszły z wdrożenia SEO u pod-zwrotnikami). W tym samym patchu domyka się też łatka bezpieczeństwa bookingu (kwota płatności).
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.37.3)
1
+ # Includio CMS Documentation (v0.37.4)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
package/ROADMAP.md CHANGED
@@ -35,6 +35,14 @@
35
35
  - [x] `[feature]` `[P1]` Slug first-class + SEO v1.0 freeze (0.26.0) — `resolveSeo` public (`includio-cms/core`), `seoFieldDescriptor` SSOT (Zod+TS drift-guard), admin URL un-hardcode (`slugPath.ts` pure resolver, no `seo`-field requirement), `SlugField`/`SeoField`/`SeoFieldData` `@public` v1.0-frozen <!-- files: src/lib/core/fields/slugPath.ts, src/lib/core/fields/seoFieldDescriptor.ts, src/lib/core/fields/resolveSeo.ts, src/lib/types/fields.ts, src/lib/admin/client/collection/collection-entries.svelte, src/lib/updates/0.26.0/ -->
36
36
  - [x] `[feature]` `[P2]` Soft-delete zamówień admin-only (0.34.0) — ukrycie zamówienia z listy bez utraty danych (`deleted_at`/`deleted_by`), kosz + przywracanie, guard `decideOrderDeletion` (safe-statuses + faktura), auto-restore przy płatności, zwolnienie rezerwacji stocku, `requireRole('admin')` <!-- files: src/lib/shop/server/orders.ts, src/lib/db-postgres/schema/shop/order.ts, src/lib/admin/remote/shop.remote.ts, src/lib/admin/client/shop/{shop-order-detail-page,shop-orders-list-page,restore-order-cell}.svelte, src/lib/updates/0.34.0/ -->
37
37
 
38
+ ## 0.37.4 — Wydajność populacji
39
+
40
+ - [x] `[feature]` `[P1]` Chuda populacja relacji (`select`/`populate`/`defaultPopulate`, 3 warstwy: call > pole > kolekcja) — `select` tnie media i relacje, `depth` cap per-pole; bloki-listy/karty-linki nie ciągną już pełnego drzewa target-wpisów. Spec: `docs/architecture/2026-07-20-lean-populate-select.md`, plan: `docs/architecture/2026-07-20-lean-populate-select.plan.md`. <!-- files: src/lib/types/{fields,collections}.ts, src/lib/core/server/fields/{populateSelect,resolveRelationFields}.ts, src/lib/core/server/entries/operations/resolveEntry.ts -->
41
+ - [x] `[fix]` `[P1]` Over-fetch wersji — meta→pick→hydrate (`getEntryVersionsMeta`/`getEntryVersionData`), pełny JSON tylko dla wybranej wersji. <!-- files: src/lib/core/server/entries/operations/versionResolve.ts, src/lib/types/adapters/db.ts, src/lib/db-postgres/index.ts -->
42
+ - [x] `[fix]` `[P1]` Indeksy ścieżki treści — `entry(slug,type)`, `entry_version(entry_id,lang,version_number / entry_id,published_at)`, `media_file_tag(tag_id)`. <!-- files: src/lib/db-postgres/schema/{entry,entryVersion,mediaFileTag}.ts -->
43
+ - [x] `[breaking]` `[P2]` Kontrakt `DatabaseAdapter` — nowe wymagane `getEntryVersionsMeta` + `getEntryVersionData` (dotyczy własnych adapterów DB).
44
+ - Backlog wydajności (odroczone C/D/E/F/G + indeksy shop/booking): `ideas/performance-backlog.md`.
45
+
38
46
  ## 0.37.1 — Bramka cenowa (bezpieczeństwo)
39
47
 
40
48
  - [x] `[fix]` `[breaking]` `[P0]` **Data wyceny zamrożona na rezerwacji** (`bookings.priced_at`). Dotąd cena bazowa była zamrożona, ale data — nie: `editBooking` i `priceFromExistingRows` (ścieżka **korekt**) przeliczały reguły zależne od czasu wg `new Date()`. Obsługa księgowała dopłatę ustaloną telefonicznie tydzień przed wylotem → `daysUntil(anchor, now)` spadało poniżej progu → **early-bird sprzed miesięcy cicho znikał, a klient robił się winien pieniądze**. Bomba tykała sama, bez żadnej zmiany cennika. Rezerwacje sprzed 0.37.1: `priced_at = NULL` → fallback na `created_at` (zero migracji danych). Pełny snapshot **wartości** polityki (`pricing_snapshot`) zostaje w 0.38.0 §4.4 — wymaga świadomej zmiany cennika przez klientkę, więc da się ją uprzedzić.
@@ -103,6 +111,18 @@ Spec: `docs/superpowers/specs/2026-07-13-moduly-kontrakt-vs-polityka-design.md`.
103
111
 
104
112
  ## Backlog
105
113
 
114
+ > **i18n / lokalizacja** — audyt stanu + porównanie z PayloadCMS: `ideas/post-v1/i18n-localization-audit.md`
115
+ > (2026-07-20). Wniosek: Paraglide zostaje, praca leży w rdzeniu. Każdy item = osobny brainstorm.
116
+ > Microcopy aplikacji osobno: `ideas/post-v1/editable-microcopy-i18n.md`.
117
+
118
+ - [ ] `[fix]` `[P0]` **Brak tłumaczenia ładuje treść innego języka** — gdy wpis nie ma wersji dla wybranego języka, `entry-version.svelte:21` przechodzi fallbackiem na `entry.versions[0]`, czyli pokazuje treść *innego* języka pod etykietą wybranego, bez sygnału w UI. Edytor przełącza na EN, widzi polski tekst, poprawia i zapisuje → nadpisał EN polszczyzną. Cicha utrata danych, nie kosmetyka. Payload pokazuje w tej sytuacji pusty slice locale. Potrzebny jawny pusty stan („brak tłumaczenia — utwórz / skopiuj z…"). Audyt §C1. <!-- files: src/lib/admin/client/entry/entry-version.svelte -->
119
+ - [ ] `[breaking]` `[P1]` **Locale config v2** — usunąć hardkod `Language = 'en'\|'pl'` (`types/languages.ts:3`, dziś blokuje trzeci język i klienta nie-PL/EN), kształt locale `{ code, label, rtl?, fallbackLocale? }` + explicit `defaultLocale` zamiast konwencji „pierwszy z tablicy", oraz łańcuch fallbacków przy odczycie zamiast twardego `ENTRY_VERSION_NOT_FOUND` (wzorzec: Payload `sanitizeFallbackLocale.ts`). Jeden spec — wspólna powierzchnia, jedno breaking. Audyt §B1-B3. <!-- files: src/lib/types/languages.ts, src/lib/types/config.ts, src/lib/core/server/entries/operations/resolveEntry.ts -->
120
+ - [ ] `[feature]` `[P1]` **Per-locale routing surface** — `resolveEntryBySlug(collection, slug, {locale})` + `alternates`/`getAlternateUrls(entry)` w rdzeniu, plus typy wyrażające relację „ten sam wpis, inny język". Dziś konsument ręcznie re-queruje CMS w drugim języku po przetłumaczony slug, owija w try/catch cicho zwracający `{}`, robi twardy `window.location.href` przy zmianie języka i sypie `as any` (moreways `[...slug]/+page.server.ts:6-19`, `navbar.svelte:11-18`). Każdy kolejny wielojęzyczny konsument wdepnie w to samo. Audyt §A1-A2. <!-- files: src/lib/core/server/entries/operations/resolveEntry.ts, src/lib/core/fields/slugPath.ts, src/lib/types/entries.ts -->
121
+ - [ ] `[feature]` `[P1]` **Wymiar języka w liście wpisów** — kolumna/wskaźnik istniejących tłumaczeń + filtr „brakuje tłumaczenia X". Dziś `getEntryStatus` = „opublikowany, jeśli którykolwiek język", więc braków tłumaczeń nie widać z listy. Audyt §C2. <!-- files: src/lib/admin/client/collection/collection-entries.svelte, src/lib/admin/client/entry/utils.ts -->
122
+ - [ ] `[fix]` `[P1]` **Historia wersji świadoma języka** — `version-history-sheet.svelte` miesza wersje wszystkich języków bez filtra i bez etykiety języka w wierszu, a nawigacja `?version=` omija switcher → można przywrócić wersję innego języka niż się sądzi. Audyt §C3. <!-- files: src/lib/admin/client/entry/version-history-sheet.svelte, entry-version.svelte -->
123
+ - [ ] `[feature]` `[P2]` **UX lokalizacji w edytorze (paczka)** — copy-from-language per-pole/„tylko puste" zamiast całościowego nadpisania; język treści w URL (`?locale=`) zamiast globalnego toggle w localStorage dzielonego przez wszystkie wpisy; pasek „Translations" liczący realne pokrycie pól, nie sam status publikacji; ujednolicenie dwóch modeli lokalizacji (wersja-per-język vs wartości kluczowane językiem w `simple-array`/`blocks`/`UrlFieldData`). Audyt §C4-C7. <!-- files: src/lib/admin/client/entry/{entry,publish-panel}.svelte, src/lib/admin/state/content-language.svelte.ts, src/lib/admin/components/fields/ -->
124
+ - [ ] `[feature]` `[P2]` **Dokończyć i18n interfejsu admina** — store `interface-language` + switcher istnieją, ale katalog Paraglide ma 3 klucze placeholder, a 59/182 plików admina ma stringi zahardkodowane po polsku → switcher nie ma czego przełączać. Odpowiednik `@payloadcms/translations`. Duży, mechaniczny koszt. Audyt §D1. <!-- files: messages/{en,pl}.json, src/lib/admin/state/interface-language.svelte.ts, src/lib/admin/ -->
125
+ - [ ] `[fix]` `[P2]` **Spójność i18n — maile formularzy + klucz języka w liście** — subject/body powiadomień formularzy to zahardkodowane literały angielskie mimo `Localized` na etykietach pól; `collection-entries.svelte:506-509` keyuje odczyt treści (a11y check) po *interface* language zamiast content language. Audyt §D2-D3. <!-- files: src/lib/core/server/forms/submissions/operations/create.ts, src/lib/admin/client/collection/collection-entries.svelte -->
106
126
  - [ ] `[feature]` `[P2]` **Panel SEO w sidebarze admina (Yoast-like)** — osobny brainstorm/spec. Guided, niestechniczny panel (odwrotność przytłaczającego Yoasta, on-brand): globalna konfiguracja SEO, per-wpisowy box (snippet preview, noindex toggle, focus keyword → wtedy świadome rozszerzenie pola `seo`), pobranie sitemapy (nakładka na `buildSitemap`). **Przekierowania (redirects)** prawdopodobnie własny spec (tabela DB + hook 301/302 + CRUD). Szwy pod to = SEO surface 0.37.2.
107
127
  - [ ] `[fix]` `[P1]` Wyciek puli połączeń DB w dev (HMR) — pojedynczy proces `vite dev` konsumenta trzymał **100 połączeń** do Postgresa (cały `max_connections`), po czym baza odbijała wszystko: `FATAL: sorry, too many clients already` (zaobserwowane 2026-07-13 na pod-zwrotnikami). Hipoteza: każde przeładowanie modułu przez HMR tworzy nowy klient `postgres.js` (pula domyślnie 10), a stary nie jest zamykany — 10 przeładowań wyczerpuje limit. Sprawdzić zamykanie klienta w `import.meta.hot.dispose` / singleton na `globalThis` w adapterze. Dotyka każdego, kto dłużej pracuje w dev. <!-- files: src/lib/db-postgres/index.ts -->
108
128
  - [ ] `[chore]` `[P1]` CMS doctor — `validateLayout()` jest napisany i przetestowany, ale nie jest wołany z żadnego miejsca produkcyjnego: literówka w nazwie pola w tabie przechodzi po cichu (pole ląduje w orphanach). Podpiąć go do `defineConfig` obok `validatePreviews`, dołożyć `pnpm cms check` w CLI i zestaw reguł zdrowia configu (martwe pola, `pathTemplate` bez `previewUrl`/`previews`, itp.). <!-- files: src/lib/core/fields/layoutUtils.ts, src/lib/sveltekit/config.ts, src/lib/core/config/, src/lib/cli/index.ts -->
@@ -117,7 +137,7 @@ Spec: `docs/superpowers/specs/2026-07-13-moduly-kontrakt-vs-polityka-design.md`.
117
137
  - [ ] `[feature]` `[P1]` Ban/unban UI — reason + expiry, `banUser`/`unbanUser` <!-- files: src/lib/admin/client/users/ban-user-dialog.svelte -->
118
138
  - [ ] `[feature]` `[P1]` Impersonation UI — impersonate/stop bar <!-- files: src/lib/admin/client/users/impersonation-bar.svelte -->
119
139
  - [ ] `[chore]` `[P2]` Caching/performance layer (scope TBD)
120
- - [ ] `[perf]` Opt-out populacji media per-pole (`populate.fields` obejmujące media, nie tylko relacje) — dziś `resolveMediaFields` wołane bez `ctx`, galeria ciągnięta na marne w widokach list. Diagnoza + opcje: `docs/architecture/2026-07-16-media-populate-opt-out.md`
140
+ - [x] `[perf]` Opt-out populacji media per-pole zrealizowane przez `select` (tnie media i relacje, 3 warstwy) w 0.37.4; notatka diagnozy wchłonięta. Spec: `docs/architecture/2026-07-20-lean-populate-select.md`
121
141
  - [ ] `[feature]` `[P2]` API/CLI for configuration (setup DX for less technical users)
122
142
  - [ ] `[feature]` `[P0]` Plugin hooks in CRUD ops (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
123
143
  - [ ] `[feature]` `[P0]` Plugin registration API — public surface for external plugins
@@ -16,8 +16,10 @@ export type ResolveStatus = 'published' | 'draft' | 'scheduled';
16
16
  export type PopulateConfig = {
17
17
  /** Hard cap on relation depth. Default 5. Use `0` to keep all relations as raw IDs. */
18
18
  maxDepth?: number;
19
- /** Per-field opt-out. `{ author: false }` keeps `author` as raw ID instead of populated Entry. */
20
- fields?: Record<string, false>;
19
+ /** Zawęź top-level entry do tych slugów pól (+ meta). Omit = wszystkie pola. */
20
+ select?: string[];
21
+ /** Per-field: `false` = surowe ID (denylist); `RelationPopulate` = per-field lean override. */
22
+ fields?: Record<string, false | import('../../../../types/fields.js').RelationPopulate>;
21
23
  };
22
24
  /**
23
25
  * Internal context passed through the populate chain.
@@ -2,18 +2,8 @@ import { getCMS } from '../../../cms.js';
2
2
  import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
3
3
  import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
4
4
  import { CmsError } from '../../../errors.js';
5
- function pickVersion(versions, status) {
6
- const sorted = versions.slice().sort((a, b) => b.versionNumber - a.versionNumber);
7
- const now = new Date();
8
- switch (status) {
9
- case 'published':
10
- return sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) ?? null;
11
- case 'draft':
12
- return sorted.find((v) => v.publishedAt == null) ?? null;
13
- case 'scheduled':
14
- return sorted.find((v) => v.publishedAt != null && v.publishedAt > now) ?? null;
15
- }
16
- }
5
+ import { pickAndHydrateVersions, pickVersionsFromMeta } from './versionResolve.js';
6
+ import { narrowFields, narrowData } from '../../fields/populateSelect.js';
17
7
  async function buildEntry(dbEntry, version, ctx) {
18
8
  const cms = getCMS();
19
9
  let config;
@@ -25,9 +15,14 @@ async function buildEntry(dbEntry, version, ctx) {
25
15
  return null;
26
16
  }
27
17
  const fields = getFieldsFromConfig(config);
18
+ // Top-level select (tylko korzeń): zawęź pola+dane przed populacją,
19
+ // żeby resolvery (media/relacje) nie dotknęły niewybranych pól.
20
+ const sel = ctx.depth === 0 ? ctx.populate.select : undefined;
21
+ const effFields = sel ? narrowFields(fields, sel) : fields;
22
+ const effData = sel ? narrowData(version.data, sel) : version.data;
28
23
  // Lazy import to avoid static circular dep with resolveRelationFields → _populate
29
24
  const { _populate } = await import('../../fields/populateEntry.js');
30
- const populated = await _populate(version.data, fields, ctx);
25
+ const populated = await _populate(effData, effFields, ctx);
31
26
  const slugPath = getEntrySlugPath(dbEntry.slug);
32
27
  const slugValue = getSlugFromEntryData(version.data, slugPath, ctx.locale);
33
28
  const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue, ctx.locale) : undefined;
@@ -82,11 +77,13 @@ export async function resolveEntry(opts) {
82
77
  return null;
83
78
  if (opts.collection && dbEntry.slug !== opts.collection)
84
79
  return null;
85
- const versions = await cms.databaseAdapter.getEntryVersions({
80
+ const versionMap = await pickAndHydrateVersions({
81
+ adapter: cms.databaseAdapter,
86
82
  entryIds: [dbEntry.id],
87
- lang: locale
83
+ lang: locale,
84
+ status
88
85
  });
89
- const picked = pickVersion(versions, status);
86
+ const picked = versionMap.get(dbEntry.id);
90
87
  if (!picked)
91
88
  return null;
92
89
  const ctx = {
@@ -143,24 +140,21 @@ export async function resolveEntries(opts) {
143
140
  if (filtered.length === 0)
144
141
  return [];
145
142
  const entryIds = filtered.map((e) => e.id);
146
- const allVersions = await cms.databaseAdapter.getEntryVersions({
143
+ const versionMap = await pickAndHydrateVersions({
144
+ adapter: cms.databaseAdapter,
147
145
  entryIds,
148
146
  lang: locale,
149
- dataValues: opts.filter?.dataValues,
150
- dataLike: opts.filter?.dataLike,
151
- dataILikeOr: opts.filter?.dataILikeOr
147
+ status,
148
+ filters: {
149
+ dataValues: opts.filter?.dataValues,
150
+ dataLike: opts.filter?.dataLike,
151
+ dataILikeOr: opts.filter?.dataILikeOr
152
+ }
152
153
  });
153
- const versionsByEntry = new Map();
154
- for (const v of allVersions) {
155
- const arr = versionsByEntry.get(v.entryId) ?? [];
156
- arr.push(v);
157
- versionsByEntry.set(v.entryId, arr);
158
- }
159
154
  const order = new Map(filtered.map((e, i) => [e.id, i]));
160
155
  const picks = [];
161
156
  for (const dbEntry of filtered) {
162
- const vs = versionsByEntry.get(dbEntry.id) ?? [];
163
- const picked = pickVersion(vs, status);
157
+ const picked = versionMap.get(dbEntry.id);
164
158
  if (picked)
165
159
  picks.push({ version: picked, dbEntry });
166
160
  }
@@ -219,24 +213,12 @@ export async function countEntries(opts) {
219
213
  if (filtered.length === 0)
220
214
  return 0;
221
215
  const entryIds = filtered.map((e) => e.id);
222
- const allVersions = await cms.databaseAdapter.getEntryVersions({
216
+ const meta = await cms.databaseAdapter.getEntryVersionsMeta({
223
217
  entryIds,
224
218
  lang: locale,
225
219
  dataValues: opts.filter?.dataValues,
226
220
  dataLike: opts.filter?.dataLike,
227
221
  dataILikeOr: opts.filter?.dataILikeOr
228
222
  });
229
- const versionsByEntry = new Map();
230
- for (const v of allVersions) {
231
- const arr = versionsByEntry.get(v.entryId) ?? [];
232
- arr.push(v);
233
- versionsByEntry.set(v.entryId, arr);
234
- }
235
- let count = 0;
236
- for (const dbEntry of filtered) {
237
- const vs = versionsByEntry.get(dbEntry.id) ?? [];
238
- if (pickVersion(vs, status))
239
- count++;
240
- }
241
- return count;
223
+ return pickVersionsFromMeta(meta, status).size;
242
224
  }
@@ -0,0 +1,39 @@
1
+ import type { DbEntryVersion } from '../../../../types/entries.js';
2
+ import type { DatabaseAdapter } from '../../../../types/adapters/db.js';
3
+ import type { ResolveStatus } from './resolveEntry.js';
4
+ type Pickable = {
5
+ versionNumber: number;
6
+ publishedAt: Date | null;
7
+ };
8
+ /**
9
+ * Wybierz wersję wg statusu.
10
+ * - `published`: najnowsza z `publishedAt <= now`
11
+ * - `draft`: najnowsza bez `publishedAt`; przy `draftFallback` → fallback na published
12
+ * - `scheduled`: najnowsza z `publishedAt > now` (bez fallbacku)
13
+ */
14
+ export declare function pickVersion<T extends Pickable>(versions: T[], status: ResolveStatus, opts?: {
15
+ draftFallback?: boolean;
16
+ }): T | null;
17
+ /** Zgrupuj metadane wersji po `entryId` i wybierz jedną wersję na wpis. */
18
+ export declare function pickVersionsFromMeta<T extends Pickable & {
19
+ entryId: string;
20
+ }>(versions: T[], status: ResolveStatus, opts?: {
21
+ draftFallback?: boolean;
22
+ }): Map<string, T>;
23
+ /**
24
+ * Meta → pick → hydrate: pobiera metadane wersji (bez `data`), wybiera jedną na wpis,
25
+ * a pełne `data` hydratuje wyłącznie dla wybranych par (entryId, versionNumber).
26
+ */
27
+ export declare function pickAndHydrateVersions(args: {
28
+ adapter: Pick<DatabaseAdapter, 'getEntryVersionsMeta' | 'getEntryVersionData'>;
29
+ entryIds: string[];
30
+ lang: string;
31
+ status: ResolveStatus;
32
+ draftFallback?: boolean;
33
+ filters?: {
34
+ dataValues?: Record<string, unknown>;
35
+ dataLike?: Record<string, unknown>;
36
+ dataILikeOr?: Record<string, unknown>;
37
+ };
38
+ }): Promise<Map<string, DbEntryVersion>>;
39
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Wybierz wersję wg statusu.
3
+ * - `published`: najnowsza z `publishedAt <= now`
4
+ * - `draft`: najnowsza bez `publishedAt`; przy `draftFallback` → fallback na published
5
+ * - `scheduled`: najnowsza z `publishedAt > now` (bez fallbacku)
6
+ */
7
+ export function pickVersion(versions, status, opts = {}) {
8
+ const sorted = versions.slice().sort((a, b) => b.versionNumber - a.versionNumber);
9
+ const now = new Date();
10
+ const published = () => sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) ?? null;
11
+ switch (status) {
12
+ case 'published':
13
+ return published();
14
+ case 'draft':
15
+ return sorted.find((v) => v.publishedAt == null) ?? (opts.draftFallback ? published() : null);
16
+ case 'scheduled':
17
+ return sorted.find((v) => v.publishedAt != null && v.publishedAt > now) ?? null;
18
+ }
19
+ }
20
+ /** Zgrupuj metadane wersji po `entryId` i wybierz jedną wersję na wpis. */
21
+ export function pickVersionsFromMeta(versions, status, opts) {
22
+ const byEntry = new Map();
23
+ for (const v of versions) {
24
+ const arr = byEntry.get(v.entryId) ?? [];
25
+ arr.push(v);
26
+ byEntry.set(v.entryId, arr);
27
+ }
28
+ const out = new Map();
29
+ for (const [entryId, vs] of byEntry) {
30
+ const picked = pickVersion(vs, status, opts);
31
+ if (picked)
32
+ out.set(entryId, picked);
33
+ }
34
+ return out;
35
+ }
36
+ /**
37
+ * Meta → pick → hydrate: pobiera metadane wersji (bez `data`), wybiera jedną na wpis,
38
+ * a pełne `data` hydratuje wyłącznie dla wybranych par (entryId, versionNumber).
39
+ */
40
+ export async function pickAndHydrateVersions(args) {
41
+ const meta = await args.adapter.getEntryVersionsMeta({
42
+ entryIds: args.entryIds,
43
+ lang: args.lang,
44
+ ...args.filters
45
+ });
46
+ const picked = pickVersionsFromMeta(meta, args.status, { draftFallback: args.draftFallback });
47
+ if (picked.size === 0)
48
+ return new Map();
49
+ const picks = [...picked.values()].map((m) => ({ entryId: m.entryId, versionNumber: m.versionNumber }));
50
+ const hydrated = await args.adapter.getEntryVersionData({ picks, lang: args.lang });
51
+ return new Map(hydrated.map((v) => [v.entryId, v]));
52
+ }
@@ -0,0 +1,22 @@
1
+ import type { Field, RelationPopulate } from '../../../types/fields.js';
2
+ import type { EntryData } from '../../../types/entries.js';
3
+ /** Efektywna konfiguracja populacji po rozwiązaniu precedencji. `null` = pełna populacja. */
4
+ export type EffectivePopulate = {
5
+ select?: string[];
6
+ depth?: number;
7
+ } | null;
8
+ /**
9
+ * Rozwiąż precedencję chudej populacji dla pojedynczego pola relacji:
10
+ * call override > pole > kolekcja docelowa > null (pełna populacja).
11
+ */
12
+ export declare function resolveEffectivePopulate(args: {
13
+ callOverride?: RelationPopulate;
14
+ fieldPopulate?: RelationPopulate;
15
+ collectionDefault?: RelationPopulate;
16
+ }): EffectivePopulate;
17
+ /** Zawęź listę definicji pól do wybranych slugów. */
18
+ export declare function narrowFields(fields: Field[], select: string[]): Field[];
19
+ /** Pick tylko wybranych kluczy z danych wpisu. */
20
+ export declare function narrowData(data: EntryData, select: string[]): EntryData;
21
+ /** Stabilny klucz cache dla `entriesMap` (kolejność `select` bez znaczenia). */
22
+ export declare function populateConfigKey(cfg: EffectivePopulate): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Rozwiąż precedencję chudej populacji dla pojedynczego pola relacji:
3
+ * call override > pole > kolekcja docelowa > null (pełna populacja).
4
+ */
5
+ export function resolveEffectivePopulate(args) {
6
+ return args.callOverride ?? args.fieldPopulate ?? args.collectionDefault ?? null;
7
+ }
8
+ /** Zawęź listę definicji pól do wybranych slugów. */
9
+ export function narrowFields(fields, select) {
10
+ const set = new Set(select);
11
+ return fields.filter((f) => set.has(f.slug));
12
+ }
13
+ /** Pick tylko wybranych kluczy z danych wpisu. */
14
+ export function narrowData(data, select) {
15
+ const set = new Set(select);
16
+ const out = {};
17
+ for (const k of Object.keys(data)) {
18
+ if (set.has(k))
19
+ out[k] = data[k];
20
+ }
21
+ return out;
22
+ }
23
+ /** Stabilny klucz cache dla `entriesMap` (kolejność `select` bez znaczenia). */
24
+ export function populateConfigKey(cfg) {
25
+ if (!cfg)
26
+ return 'FULL';
27
+ return JSON.stringify({ select: cfg.select ? [...cfg.select].sort() : undefined, depth: cfg.depth });
28
+ }
@@ -1,13 +1,13 @@
1
- import type { EntryData, PopulatedEntryData, DbEntryVersion } from '../../../types/entries.js';
1
+ import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
2
2
  import type { Field } from '../../../types/fields.js';
3
3
  import type { PopulateCtx, ResolveStatus } from '../entries/operations/resolveEntry.js';
4
4
  /**
5
- * Version of a *related* entry to populate. Exported for tests.
6
- *
7
- * `draft` falls back to the published version: a related entry that has never been
8
- * drafted (only published versions) must still resolve, otherwise draft previews
9
- * silently drop every relation to `null` — see preview.remote.ts, which populates
10
- * form data with `status: 'draft'`.
5
+ * Wersja *populowanej relacji*: draft zawsze z fallbackiem na published — relacja,
6
+ * która nigdy nie była szkicowana (tylko published), musi się rozwiązać, inaczej
7
+ * podgląd szkicu cicho zeruje każdą relację (patrz preview.remote.ts). Eksport dla testów.
11
8
  */
12
- export declare function pickVersion(versions: DbEntryVersion[], status: ResolveStatus): DbEntryVersion | null;
9
+ export declare function pickVersion<T extends {
10
+ versionNumber: number;
11
+ publishedAt: Date | null;
12
+ }>(versions: T[], status: ResolveStatus): T | null;
13
13
  export declare function resolveRelationFields(data: EntryData, fields: Field[], ctx: PopulateCtx): Promise<PopulatedEntryData>;