smoodly 0.0.38 → 0.0.40
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/cli/space-io.d.ts +5 -4
- package/dist/cli/space-io.js +12 -5
- package/dist/client.js +15 -1
- package/dist/content/content-api.d.ts +10 -0
- package/dist/content/content-api.js +27 -3
- package/dist/env.d.ts +5 -0
- package/dist/env.js +22 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/page-renderer.js +17 -7
- 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
|
}),
|
package/dist/cli/space-io.d.ts
CHANGED
|
@@ -7,10 +7,11 @@ export type SpaceConnection = {
|
|
|
7
7
|
export declare const MEDIA_BUCKET = "media";
|
|
8
8
|
/** Rows per request. A host may cap below it, so a read stops at the first empty page, not the first short one. */
|
|
9
9
|
export declare const PAGE_ROWS = 1000;
|
|
10
|
-
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). */
|
|
11
|
-
export declare function loadAppEnv(cwd: string, env?: Record<string, string | undefined>):
|
|
12
|
-
/** The app's content client, from its env.
|
|
13
|
-
|
|
10
|
+
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
|
|
11
|
+
export declare function loadAppEnv(cwd: string, env?: Record<string, string | undefined>): string[];
|
|
12
|
+
/** The app's content client, from its env. Run outside the app, the missing keys are a missing
|
|
13
|
+
* folder, so the error names the folder it looked in. */
|
|
14
|
+
export declare function connectFromEnv(cwd: string, env?: Record<string, string | undefined>): SpaceConnection;
|
|
14
15
|
export { readSchemaVersion } from "../schema-read.ts";
|
|
15
16
|
export declare function schemaRefusal(missing: "key" | "schema", command: "Export" | "Import"): string;
|
|
16
17
|
/** Every row of one table in one space, paged in primary-key order. The space filter is explicit: a service-role key sees every space. */
|
package/dist/cli/space-io.js
CHANGED
|
@@ -13,22 +13,29 @@ import { ROW_KEY } from "../space-transfer.js";
|
|
|
13
13
|
export const MEDIA_BUCKET = "media";
|
|
14
14
|
/** Rows per request. A host may cap below it, so a read stops at the first empty page, not the first short one. */
|
|
15
15
|
export const PAGE_ROWS = 1000;
|
|
16
|
-
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). */
|
|
16
|
+
/** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
|
|
17
17
|
export function loadAppEnv(cwd, env = process.env) {
|
|
18
|
+
const read = [];
|
|
18
19
|
for (const name of [".env.local", ".env"]) {
|
|
19
20
|
const file = join(cwd, name);
|
|
20
21
|
if (!existsSync(file))
|
|
21
22
|
continue;
|
|
23
|
+
read.push(name);
|
|
22
24
|
for (const [key, value] of Object.entries(parseEnv(readFileSync(file, "utf8")))) {
|
|
23
25
|
if (env[key] === undefined)
|
|
24
26
|
env[key] = value;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
29
|
+
return read;
|
|
27
30
|
}
|
|
28
|
-
/** The app's content client, from its env.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
/** The app's content client, from its env. Run outside the app, the missing keys are a missing
|
|
32
|
+
* folder, so the error names the folder it looked in. */
|
|
33
|
+
export function connectFromEnv(cwd, env = process.env) {
|
|
34
|
+
const read = loadAppEnv(cwd, env);
|
|
35
|
+
if (read.length === 0 && (!env.SMOODLY_URL || !env.SMOODLY_SECRET_KEY)) {
|
|
36
|
+
throw new Error(`smoodly: no .env.local or .env in ${cwd} — run this in your app's folder.`);
|
|
37
|
+
}
|
|
38
|
+
return { db: createSmoodlyClient(resolveSmoodlyEnv(env)) };
|
|
32
39
|
}
|
|
33
40
|
export { readSchemaVersion } from "../schema-read.js";
|
|
34
41
|
export function schemaRefusal(missing, command) {
|
package/dist/client.js
CHANGED
|
@@ -11,11 +11,25 @@ export function clientOptions(env) {
|
|
|
11
11
|
// without one (spec 2026-09-30 tester findings §4): a self-host service role is a valid apikey.
|
|
12
12
|
return { url: env.url, key: env.publishableKey || env.secretKey, headers: { Authorization: `Bearer ${env.secretKey}` } };
|
|
13
13
|
}
|
|
14
|
+
/** fetch that names the address when the network fails: supabase-js reports only
|
|
15
|
+
* "TypeError: fetch failed", which names neither the variable nor the URL. */
|
|
16
|
+
function namedFetch(url) {
|
|
17
|
+
return async (input, init) => {
|
|
18
|
+
try {
|
|
19
|
+
return await fetch(input, init);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
23
|
+
throw e;
|
|
24
|
+
throw new TypeError(`can't reach SMOODLY_URL (${url}): ${e instanceof Error ? e.message : String(e)}`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
14
28
|
/** The server-side client the stores are built on. */
|
|
15
29
|
export function createSmoodlyClient(env) {
|
|
16
30
|
const options = clientOptions(env);
|
|
17
31
|
return createClient(options.url, options.key, {
|
|
18
|
-
global: { headers: options.headers },
|
|
32
|
+
global: { headers: options.headers, fetch: namedFetch(options.url) },
|
|
19
33
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
20
34
|
});
|
|
21
35
|
}
|
|
@@ -43,6 +43,12 @@ export declare function createSmoodlyContent(deps: ContentDeps): {
|
|
|
43
43
|
title?: string;
|
|
44
44
|
}): Promise<PageRecord>;
|
|
45
45
|
publish: (id: string, locale: string) => Promise<PageRecord>;
|
|
46
|
+
/** Every page present in `locale` (the default language unless given), code-owned ones included and
|
|
47
|
+
* folders left out, optionally only one page type: what a script walks to change many pages. */
|
|
48
|
+
list(options?: {
|
|
49
|
+
locale?: string;
|
|
50
|
+
template?: string;
|
|
51
|
+
}): Promise<PageRecord[]>;
|
|
46
52
|
/** A code-owned address with no record gets it created the way the admin's Pages list does (spec §2). */
|
|
47
53
|
find(locale: string, path: string): Promise<PageRecord | null>;
|
|
48
54
|
};
|
|
@@ -54,6 +60,10 @@ export declare function createSmoodlyContent(deps: ContentDeps): {
|
|
|
54
60
|
fields?: Record<string, unknown>;
|
|
55
61
|
}): Promise<EntryRecord>;
|
|
56
62
|
publish: (collection: string, id: string, locale: string) => Promise<EntryRecord>;
|
|
63
|
+
list: (collection: string, options?: {
|
|
64
|
+
where?: Record<string, unknown>;
|
|
65
|
+
locale?: string;
|
|
66
|
+
}) => Promise<EntryRecord[]>;
|
|
57
67
|
find(collection: string, match: {
|
|
58
68
|
slug: string;
|
|
59
69
|
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)),
|
|
@@ -88,6 +107,12 @@ export function createSmoodlyContent(deps) {
|
|
|
88
107
|
}));
|
|
89
108
|
},
|
|
90
109
|
publish: (id, locale) => must(ops.pages.publish(id, locale)),
|
|
110
|
+
/** Every page present in `locale` (the default language unless given), code-owned ones included and
|
|
111
|
+
* folders left out, optionally only one page type: what a script walks to change many pages. */
|
|
112
|
+
async list(options = {}) {
|
|
113
|
+
const locale = options.locale ?? def;
|
|
114
|
+
return (await must(ops.pages.list())).filter((p) => p.template !== null && p.locales[locale] !== undefined && (options.template === undefined || p.template === options.template));
|
|
115
|
+
},
|
|
91
116
|
/** A code-owned address with no record gets it created the way the admin's Pages list does (spec §2). */
|
|
92
117
|
async find(locale, path) {
|
|
93
118
|
const found = await findPage(locale, path);
|
|
@@ -105,10 +130,9 @@ export function createSmoodlyContent(deps) {
|
|
|
105
130
|
return options.fields ? updateEntry(collection, id, locale, options.fields) : added;
|
|
106
131
|
},
|
|
107
132
|
publish: (collection, id, locale) => must(ops.entries.setStatus(collection, id, locale, "published")),
|
|
133
|
+
list: listEntries,
|
|
108
134
|
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;
|
|
135
|
+
return (await listEntries(collection, { where: { slug: match.slug }, locale: match.locale }))[0] ?? null;
|
|
112
136
|
},
|
|
113
137
|
},
|
|
114
138
|
globals: {
|
package/dist/env.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export type SmoodlyEnv = {
|
|
|
15
15
|
* publishable key, so their callers pass "admin" (spec 2026-09-30 tester findings §4). A key
|
|
16
16
|
* in the wrong variable or of no known form is named here, not by a failed read later. */
|
|
17
17
|
export declare function resolveSmoodlyEnv(env: Record<string, string | undefined>, need?: "site" | "admin"): SmoodlyEnv;
|
|
18
|
+
/** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
|
|
19
|
+
* image links are absolute. A value without a scheme is named here, not by a bare "Invalid URL" on
|
|
20
|
+
* every page; an unset one falls back to localhost and warns once in production, where it would
|
|
21
|
+
* put localhost into every page's head. */
|
|
22
|
+
export declare function siteUrl(env?: Record<string, string | undefined>): URL;
|
|
18
23
|
/** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
|
|
19
24
|
* so `next build` without env still succeeds. */
|
|
20
25
|
export declare const smoodlyEnv: (need?: "site" | "admin") => SmoodlyEnv;
|
package/dist/env.js
CHANGED
|
@@ -41,8 +41,30 @@ export function resolveSmoodlyEnv(env, need = "site") {
|
|
|
41
41
|
throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY is neither an sb_publishable_ key nor a JWT — copy the publishable (anon) key from your Supabase project.");
|
|
42
42
|
}
|
|
43
43
|
const url = env.SMOODLY_URL;
|
|
44
|
+
if (!/^https?:\/\/[^/\s]+/.test(url)) {
|
|
45
|
+
throw new Error(`smoodly: SMOODLY_URL "${url}" is not an http(s) address — copy the project URL from your Supabase project.`);
|
|
46
|
+
}
|
|
44
47
|
return { url, secretKey, publishableKey, authUrl: env.SMOODLY_AUTH_URL || url, authKey: env.SMOODLY_AUTH_KEY || publishableKey };
|
|
45
48
|
}
|
|
49
|
+
let siteUrlWarned = false;
|
|
50
|
+
/** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
|
|
51
|
+
* image links are absolute. A value without a scheme is named here, not by a bare "Invalid URL" on
|
|
52
|
+
* every page; an unset one falls back to localhost and warns once in production, where it would
|
|
53
|
+
* put localhost into every page's head. */
|
|
54
|
+
export function siteUrl(env = process.env) {
|
|
55
|
+
const value = env.SITE_URL;
|
|
56
|
+
if (!value) {
|
|
57
|
+
if (env.NODE_ENV === "production" && !siteUrlWarned) {
|
|
58
|
+
siteUrlWarned = true;
|
|
59
|
+
console.warn(`smoodly: SITE_URL is not set — the page head's links point at http://localhost:3000 (Deploying: ${DEPLOYING}).`);
|
|
60
|
+
}
|
|
61
|
+
return new URL("http://localhost:3000");
|
|
62
|
+
}
|
|
63
|
+
if (!/^https?:\/\/[^/\s]+/.test(value)) {
|
|
64
|
+
throw new Error(`smoodly: SITE_URL "${value}" is not an http(s) address — set it like https://www.example.com.`);
|
|
65
|
+
}
|
|
66
|
+
return new URL(value);
|
|
67
|
+
}
|
|
46
68
|
/** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
|
|
47
69
|
* so `next build` without env still succeeds. */
|
|
48
70
|
export const smoodlyEnv = (need = "site") => resolveSmoodlyEnv(process.env, need);
|
package/dist/index.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ export declare const smoodly: {
|
|
|
129
129
|
};
|
|
130
130
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
131
131
|
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
132
|
-
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.ts";
|
|
132
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.ts";
|
|
133
133
|
export type { SmoodlyEnv, PresenceConfig } from "./env.ts";
|
|
134
134
|
export { clientOptions, createSmoodlyClient } from "./client.ts";
|
|
135
135
|
export { createSmoodlySite, pageContextOf } from "./site.ts";
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,6 @@ export const smoodly = {
|
|
|
67
67
|
marks,
|
|
68
68
|
};
|
|
69
69
|
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
70
|
-
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig } from "./env.js";
|
|
70
|
+
export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.js";
|
|
71
71
|
export { clientOptions, createSmoodlyClient } from "./client.js";
|
|
72
72
|
export { createSmoodlySite, pageContextOf } from "./site.js";
|
|
@@ -76,15 +76,19 @@ export function createPageRenderer(options) {
|
|
|
76
76
|
}
|
|
77
77
|
return _jsx(View, { entry: toEntryItem(hit.entry, hit.entry.fields, hit) });
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
79
|
+
/** The page's own address as its canonical, and hreflang from the SAME map a language switch reads
|
|
80
|
+
* (spec 2026-09-15 §3): the locales with a URL, through href. Next makes both absolute from the
|
|
81
|
+
* layout's `metadataBase` (tester run on 0.0.39, mission 6). */
|
|
82
|
+
function addresses(hit, locale) {
|
|
83
|
+
const own = hit.locales[locale]?.path;
|
|
84
|
+
const out = own ? { canonical: href(own, locale) } : {};
|
|
81
85
|
if (site.locales.supported.length < 2)
|
|
82
|
-
return
|
|
86
|
+
return out;
|
|
83
87
|
const languages = {};
|
|
84
|
-
for (const [
|
|
88
|
+
for (const [l, alt] of Object.entries(hit.locales))
|
|
85
89
|
if (alt?.path)
|
|
86
|
-
languages[
|
|
87
|
-
return {
|
|
90
|
+
languages[l] = href(alt.path, l);
|
|
91
|
+
return { ...out, languages };
|
|
88
92
|
}
|
|
89
93
|
// Next's cacheComponents serves the catch-all from a fallback shell that
|
|
90
94
|
// commits 200 before the page renders, so 404s and 308s are lost (spec
|
|
@@ -137,7 +141,13 @@ export function createPageRenderer(options) {
|
|
|
137
141
|
return {};
|
|
138
142
|
// A page type without seo: true still gets its page title as <title>; seo: true adds the panel's values on top.
|
|
139
143
|
const seo = hit.seo ?? (hit.kind === "page" ? { title: hit.title } : undefined);
|
|
140
|
-
|
|
144
|
+
const meta = metadataFromSeo(seo);
|
|
145
|
+
const { canonical, languages } = addresses(hit, locale);
|
|
146
|
+
// og:url only beside Open Graph values the page already sets: an openGraph of its own would
|
|
147
|
+
// replace the layout's defaults (Next merges metadata one key deep).
|
|
148
|
+
if (canonical && meta.openGraph)
|
|
149
|
+
meta.openGraph = { ...meta.openGraph, url: canonical };
|
|
150
|
+
return canonical || languages ? { ...meta, alternates: { ...(canonical ? { canonical } : {}), ...(languages ? { languages } : {}) } } : meta;
|
|
141
151
|
}
|
|
142
152
|
const any = () => true;
|
|
143
153
|
/** The escape hatch may only render ITS OWN registration's record. */
|
|
@@ -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.40";
|
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.40";
|
package/package.json
CHANGED