smoodly 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/ops-impl.js +14 -2
- package/dist/content/content-api.d.ts +4 -0
- package/dist/content/content-api.js +21 -3
- package/dist/supabase-entry-store.js +53 -37
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/admin/ops-impl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { entryErrors, entryFieldsIn, entryTitleIn } from "../entry-store.js";
|
|
2
|
-
import { chainOf, isSegment, pagePathIn } from "../paths.js";
|
|
2
|
+
import { chainOf, isSegment, pagePathIn, slugify } from "../paths.js";
|
|
3
3
|
import { servedAnchor } from "../anchors.js";
|
|
4
4
|
import { canonicalTags, distinctTags } from "../tags.js";
|
|
5
5
|
import { can, isMemberRole, normalizeRole } from "../permissions.js";
|
|
@@ -112,6 +112,18 @@ const classify = (e) => {
|
|
|
112
112
|
return err("conflict", message);
|
|
113
113
|
return err("internal", message);
|
|
114
114
|
};
|
|
115
|
+
/** A new entry without a slug gets one from the field `f.slug({ from })` follows, as the admin's form
|
|
116
|
+
* fills it (spec 2026-09-28 ASCII slugs §5), so a script needs no slug of its own. A given slug stays. */
|
|
117
|
+
function followSlug(fields, descriptors) {
|
|
118
|
+
const d = descriptors.slug;
|
|
119
|
+
if (d?.type !== "slug" || d.from === undefined)
|
|
120
|
+
return fields;
|
|
121
|
+
if (typeof fields.slug === "string" && fields.slug !== "")
|
|
122
|
+
return fields;
|
|
123
|
+
const source = fields[d.from];
|
|
124
|
+
const slug = typeof source === "string" ? slugify(source) : "";
|
|
125
|
+
return slug === "" ? fields : { ...fields, slug };
|
|
126
|
+
}
|
|
115
127
|
export function createAdminOps(deps) {
|
|
116
128
|
const { config, pages, entries, effects } = deps;
|
|
117
129
|
const href = deps.href ?? ((path) => path);
|
|
@@ -783,7 +795,7 @@ export function createAdminOps(deps) {
|
|
|
783
795
|
const descriptors = collectionFields(collection);
|
|
784
796
|
if (!descriptors)
|
|
785
797
|
return err("not_found", `smoodly: no collection named "${collection}".`);
|
|
786
|
-
const created = await entries.create(collection, withDefaults(await foldTags(collection, input.locale, input.fields, descriptors), descriptors), { locale: input.locale, parentId: input.parentId ?? null }, by);
|
|
798
|
+
const created = await entries.create(collection, withDefaults(await foldTags(collection, input.locale, followSlug(input.fields, descriptors), descriptors), descriptors), { locale: input.locale, parentId: input.parentId ?? null }, by);
|
|
787
799
|
expireCollection(collection);
|
|
788
800
|
return ok(created);
|
|
789
801
|
}),
|
|
@@ -54,6 +54,10 @@ export declare function createSmoodlyContent(deps: ContentDeps): {
|
|
|
54
54
|
fields?: Record<string, unknown>;
|
|
55
55
|
}): Promise<EntryRecord>;
|
|
56
56
|
publish: (collection: string, id: string, locale: string) => Promise<EntryRecord>;
|
|
57
|
+
list: (collection: string, options?: {
|
|
58
|
+
where?: Record<string, unknown>;
|
|
59
|
+
locale?: string;
|
|
60
|
+
}) => Promise<EntryRecord[]>;
|
|
57
61
|
find(collection: string, match: {
|
|
58
62
|
slug: string;
|
|
59
63
|
locale?: string;
|
|
@@ -65,6 +65,25 @@ export function createSmoodlyContent(deps) {
|
|
|
65
65
|
async function updateEntry(collection, id, locale, fields) {
|
|
66
66
|
return must(ops.entries.save(collection, id, locale, fields));
|
|
67
67
|
}
|
|
68
|
+
/** The entries present in `locale` (the default language unless given) whose fields match every `where` key:
|
|
69
|
+
* shared and that locale's fields and its slug, equal, or held by an array field (tags, refList). */
|
|
70
|
+
async function listEntries(collection, options = {}) {
|
|
71
|
+
if (!deps.config.registry.collections?.some((c) => c.name === collection)) {
|
|
72
|
+
throw new SmoodlyContentError({ code: "not_found", message: `smoodly: no collection named "${collection}".` });
|
|
73
|
+
}
|
|
74
|
+
const locale = options.locale ?? def;
|
|
75
|
+
const where = Object.entries(options.where ?? {});
|
|
76
|
+
return (await must(ops.entries.list(collection))).filter((e) => {
|
|
77
|
+
const row = e.locales[locale];
|
|
78
|
+
if (!row)
|
|
79
|
+
return false;
|
|
80
|
+
const fields = { ...e.fields, ...row.fields, slug: row.slug };
|
|
81
|
+
return where.every(([key, want]) => {
|
|
82
|
+
const value = fields[key];
|
|
83
|
+
return Array.isArray(value) ? value.includes(want) : value === want;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
68
87
|
return {
|
|
69
88
|
pages: {
|
|
70
89
|
create: (input) => must(ops.pages.create(input)),
|
|
@@ -105,10 +124,9 @@ export function createSmoodlyContent(deps) {
|
|
|
105
124
|
return options.fields ? updateEntry(collection, id, locale, options.fields) : added;
|
|
106
125
|
},
|
|
107
126
|
publish: (collection, id, locale) => must(ops.entries.setStatus(collection, id, locale, "published")),
|
|
127
|
+
list: listEntries,
|
|
108
128
|
async find(collection, match) {
|
|
109
|
-
|
|
110
|
-
const all = await must(ops.entries.list(collection));
|
|
111
|
-
return all.find((e) => e.locales[locale]?.slug === match.slug) ?? null;
|
|
129
|
+
return (await listEntries(collection, { where: { slug: match.slug }, locale: match.locale }))[0] ?? null;
|
|
112
130
|
},
|
|
113
131
|
},
|
|
114
132
|
globals: {
|
|
@@ -48,6 +48,8 @@ function toRecord(row) {
|
|
|
48
48
|
function toVersion(row) {
|
|
49
49
|
return { id: row.id, entryId: row.entry_id, locale: row.locale, fields: row.fields, savedAt: Date.parse(row.saved_at) };
|
|
50
50
|
}
|
|
51
|
+
/** Rows per request when `list` reads a whole collection: under PostgREST's default cap. */
|
|
52
|
+
const LIST_PAGE = 1000;
|
|
51
53
|
export class SupabaseEntryStore {
|
|
52
54
|
constructor(db, collections, options = {}) {
|
|
53
55
|
this.db = db;
|
|
@@ -391,47 +393,61 @@ export class SupabaseEntryStore {
|
|
|
391
393
|
// EVERY locale row of each record (spec 2026-09-11 §3), ordered by locale
|
|
392
394
|
// so a record's key order never depends on physical row order.
|
|
393
395
|
const gate = options?.status !== undefined || options?.locale !== undefined;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
396
|
+
// Built per request: without a `limit` the list is read in pages, because
|
|
397
|
+
// PostgREST caps one response (1000 rows by default) without saying so.
|
|
398
|
+
const build = () => {
|
|
399
|
+
let query = this.db
|
|
400
|
+
.from("entries")
|
|
401
|
+
.select(gate ? `${SELECT}, gate:entry_locales!inner(locale)` : SELECT)
|
|
402
|
+
.eq("collection", collection)
|
|
403
|
+
.order("locale", { referencedTable: "entry_locales", ascending: true });
|
|
404
|
+
if (gate) {
|
|
405
|
+
query = query.eq("gate.locale", options.locale ?? this.locales.default);
|
|
406
|
+
if (options.status !== undefined)
|
|
407
|
+
query = query.eq("gate.status", options.status);
|
|
408
|
+
}
|
|
409
|
+
if (options?.parent !== undefined) {
|
|
410
|
+
query = options.parent === null ? query.is("parent_id", null) : query.eq("parent_id", options.parent);
|
|
411
|
+
}
|
|
412
|
+
for (const [key, want] of Object.entries(options?.where ?? {})) {
|
|
413
|
+
// Any-of compares the text form (`->>`). A scalar is JSONB containment;
|
|
414
|
+
// an array-valued field (checkboxes, refList, list, tags) contains `[value]`,
|
|
415
|
+
// a scalar field `value` — the memory adapter's matchesWhere agrees.
|
|
416
|
+
if (Array.isArray(want)) {
|
|
417
|
+
query = query.in(`fields->>${key}`, want.map(String));
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
const inArray = ARRAY_FIELD_TYPES.has(schema.fields[key]?.type ?? "");
|
|
421
|
+
query = query.contains("fields", { [key]: inArray ? [want] : want });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const order = options?.order ?? collectionOrder(schema);
|
|
425
|
+
if (order === "manual") {
|
|
426
|
+
// `id` last in both orders: a stable order, so no row falls between two pages.
|
|
427
|
+
query = query.order("sort", { ascending: true, nullsFirst: false }).order("created_at", { ascending: true }).order("id", { ascending: true });
|
|
413
428
|
}
|
|
414
429
|
else {
|
|
415
|
-
|
|
416
|
-
|
|
430
|
+
// `fields->x`, not `->>x`: ordering by the jsonb VALUE of the SHARED
|
|
431
|
+
// field (an order field is never localized), matching the memory comparator.
|
|
432
|
+
const column = order.by === "createdAt" ? "created_at" : order.by === "updatedAt" ? "updated_at" : `fields->${order.by}`;
|
|
433
|
+
query = query.order(column, { ascending: order.direction === "asc", nullsFirst: false }).order("id", { ascending: true });
|
|
417
434
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// field (an order field is never localized), matching the memory comparator.
|
|
426
|
-
const column = order.by === "createdAt" ? "created_at" : order.by === "updatedAt" ? "updated_at" : `fields->${order.by}`;
|
|
427
|
-
query = query.order(column, { ascending: order.direction === "asc", nullsFirst: false }).order("id", { ascending: true });
|
|
428
|
-
}
|
|
435
|
+
return query;
|
|
436
|
+
};
|
|
437
|
+
const rows = (res) => {
|
|
438
|
+
if (res.error)
|
|
439
|
+
this.fail(res.error.message);
|
|
440
|
+
return res.data.map(({ gate: _gate, ...row }) => toRecord(row));
|
|
441
|
+
};
|
|
429
442
|
if (typeof options?.limit === "number")
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
443
|
+
return rows(await build().limit(options.limit));
|
|
444
|
+
const all = [];
|
|
445
|
+
for (let from = 0;; from += LIST_PAGE) {
|
|
446
|
+
const page = rows(await build().range(from, from + LIST_PAGE - 1));
|
|
447
|
+
all.push(...page);
|
|
448
|
+
if (page.length < LIST_PAGE)
|
|
449
|
+
return all;
|
|
450
|
+
}
|
|
435
451
|
}
|
|
436
452
|
async query(collection, q) {
|
|
437
453
|
const schema = this.schema(collection);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.39";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.39";
|
package/package.json
CHANGED