kizlo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/adapters/auth.d.ts +18 -0
- package/dist/adapters/auth.js +15 -0
- package/dist/adapters/captcha.d.ts +28 -0
- package/dist/adapters/captcha.js +86 -0
- package/dist/adapters/geo.d.ts +18 -0
- package/dist/adapters/geo.js +18 -0
- package/dist/adapters/geo.vercel.d.ts +6 -0
- package/dist/adapters/geo.vercel.js +41 -0
- package/dist/adapters/logger.d.ts +31 -0
- package/dist/adapters/logger.js +30 -0
- package/dist/cli/banner.js +26 -0
- package/dist/cli/commands/generate.js +34 -0
- package/dist/cli/commands/init.js +313 -0
- package/dist/cli/commands/test.js +68 -0
- package/dist/cli/commands/watch.js +53 -0
- package/dist/cli/commands/wp.js +66 -0
- package/dist/cli/daemon/config.js +87 -0
- package/dist/cli/daemon/generate.js +40 -0
- package/dist/cli/daemon/lock.js +31 -0
- package/dist/cli/daemon/logger.js +8 -0
- package/dist/cli/daemon/watch.js +37 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/presets/base.js +23 -0
- package/dist/cli/presets/index.js +16 -0
- package/dist/cli/presets/nextjs.js +39 -0
- package/dist/cli/utils.js +182 -0
- package/dist/cli/wp/bootstrap.js +114 -0
- package/dist/cli/wp/compose/docker-compose.yml +73 -0
- package/dist/cli/wp/constants.js +52 -0
- package/dist/cli/wp/docker.d.ts +24 -0
- package/dist/cli/wp/docker.js +69 -0
- package/dist/cli/wp/setup.js +39 -0
- package/dist/cli/wp/types.d.ts +72 -0
- package/dist/cli/wp/types.js +8 -0
- package/dist/cli/wp/utils.d.ts +39 -0
- package/dist/cli/wp/utils.js +105 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.js +55 -0
- package/dist/comment/errors.js +47 -0
- package/dist/comment/index.js +138 -0
- package/dist/comment/schema.js +72 -0
- package/dist/comment/utils.js +35 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +7 -0
- package/dist/context.d.ts +73 -0
- package/dist/context.js +145 -0
- package/dist/cookie.d.ts +19 -0
- package/dist/cookie.js +25 -0
- package/dist/email/service.d.ts +11 -0
- package/dist/email/service.interface.d.ts +8 -0
- package/dist/email/service.js +20 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +31 -0
- package/dist/integrations/nextjs/kizlo.client.d.ts +10 -0
- package/dist/integrations/nextjs/kizlo.client.js +13 -0
- package/dist/integrations/nextjs/kizlo.server.d.ts +12 -0
- package/dist/integrations/nextjs/kizlo.server.js +13 -0
- package/dist/integrations/nextjs/utils.js +11 -0
- package/dist/kizlo.d.ts +73 -0
- package/dist/kizlo.js +144 -0
- package/dist/menu/errors.js +16 -0
- package/dist/menu/fixture.d.ts +8 -0
- package/dist/menu/fixture.js +62 -0
- package/dist/menu/index.js +70 -0
- package/dist/menu/schema.js +70 -0
- package/dist/menu/utils.js +57 -0
- package/dist/nextjs/server.d.ts +2 -0
- package/dist/nextjs/server.js +3 -0
- package/dist/nextjs.d.ts +2 -0
- package/dist/nextjs.js +3 -0
- package/dist/package.js +5 -0
- package/dist/post/error.js +30 -0
- package/dist/post/fixture.d.ts +8 -0
- package/dist/post/fixture.js +71 -0
- package/dist/post/index.js +123 -0
- package/dist/post/schema.js +120 -0
- package/dist/post/utils.js +42 -0
- package/dist/post-type/service.js +48 -0
- package/dist/router.d.ts +624 -0
- package/dist/router.js +15 -0
- package/dist/seo/index.js +54 -0
- package/dist/seo/schema.d.ts +196 -0
- package/dist/seo/schema.js +96 -0
- package/dist/seo/types.d.ts +80 -0
- package/dist/seo/utils.js +44 -0
- package/dist/service.d.ts +17 -0
- package/dist/service.js +17 -0
- package/dist/shared/constants.js +10 -0
- package/dist/shared/contract.d.ts +21 -0
- package/dist/shared/contract.js +43 -0
- package/dist/shared/crypto.d.ts +28 -0
- package/dist/shared/crypto.js +34 -0
- package/dist/shared/error.d.ts +128 -0
- package/dist/shared/error.js +147 -0
- package/dist/shared/extension.d.ts +25 -0
- package/dist/shared/extension.js +7 -0
- package/dist/shared/identifier.d.ts +11 -0
- package/dist/shared/identifier.js +26 -0
- package/dist/shared/middleware.d.ts +29 -0
- package/dist/shared/procedure.d.ts +114 -0
- package/dist/shared/procedure.js +114 -0
- package/dist/shared/result.d.ts +24 -0
- package/dist/shared/result.js +49 -0
- package/dist/shared/router.d.ts +7 -0
- package/dist/shared/router.js +26 -0
- package/dist/shared/schema.d.ts +55 -0
- package/dist/shared/schema.js +43 -0
- package/dist/shared/serialize.d.ts +12 -0
- package/dist/shared/serialize.js +51 -0
- package/dist/shared/types.d.ts +35 -0
- package/dist/shared/utils.js +17 -0
- package/dist/test/auth.d.ts +7 -0
- package/dist/test/auth.js +15 -0
- package/dist/test/captcha.d.ts +7 -0
- package/dist/test/captcha.js +8 -0
- package/dist/test/client.d.ts +11 -0
- package/dist/test/client.js +14 -0
- package/dist/test/fixtures.d.ts +14 -0
- package/dist/test/fixtures.js +13 -0
- package/dist/test/server.d.ts +10 -0
- package/dist/test/server.js +32 -0
- package/dist/test/users.d.ts +14 -0
- package/dist/test/users.js +17 -0
- package/dist/test.d.ts +12 -0
- package/dist/test.js +13 -0
- package/dist/webhook/index.d.ts +38 -0
- package/dist/webhook/index.js +44 -0
- package/dist/webhook/schema.d.ts +45 -0
- package/dist/webhook/schema.js +72 -0
- package/dist/webhook/utils.js +21 -0
- package/dist/wordpress/comment/service.d.ts +28 -0
- package/dist/wordpress/comment/service.js +55 -0
- package/dist/wordpress/comment/types.d.ts +206 -0
- package/dist/wordpress/constants.d.ts +14 -0
- package/dist/wordpress/constants.js +13 -0
- package/dist/wordpress/error.d.ts +9 -0
- package/dist/wordpress/error.js +12 -0
- package/dist/wordpress/index.d.ts +15 -0
- package/dist/wordpress/menu/items/service.d.ts +28 -0
- package/dist/wordpress/menu/items/service.js +55 -0
- package/dist/wordpress/menu/items/types.d.ts +214 -0
- package/dist/wordpress/menu/service.d.ts +30 -0
- package/dist/wordpress/menu/service.js +58 -0
- package/dist/wordpress/menu/types.d.ts +115 -0
- package/dist/wordpress/post/service.d.ts +28 -0
- package/dist/wordpress/post/service.js +55 -0
- package/dist/wordpress/post/types.d.ts +290 -0
- package/dist/wordpress/service.d.ts +77 -0
- package/dist/wordpress/service.js +180 -0
- package/dist/wordpress/types.d.ts +67 -0
- package/dist/wordpress/user/service.d.ts +28 -0
- package/dist/wordpress/user/service.js +55 -0
- package/dist/wordpress/user/types.d.ts +221 -0
- package/dist/wordpress/utils.d.ts +7 -0
- package/dist/wordpress/utils.js +9 -0
- package/package.json +109 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { WordPressService } from "../../wordpress/service.js";
|
|
2
|
+
import { credentialsPath } from "./utils.js";
|
|
3
|
+
import { bootstrapWp } from "./bootstrap.js";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/wp/setup.ts
|
|
8
|
+
/**
|
|
9
|
+
* Phase 2 — after bootstrap, build a `SeedContext` and run each `Fixture.seed`
|
|
10
|
+
* over REST, collecting namespaced fixtures, then write the credentials artifact
|
|
11
|
+
* at `<config-dir>/.kizlo/test-credentials.json`. Returns a teardown that runs
|
|
12
|
+
* each fixture's `cleanup`.
|
|
13
|
+
*/
|
|
14
|
+
async function runSeeds(config) {
|
|
15
|
+
const boot = await bootstrapWp(config);
|
|
16
|
+
const ctx = {
|
|
17
|
+
service: new WordPressService({ credentials: {
|
|
18
|
+
url: boot.url,
|
|
19
|
+
username: boot.users.admin.username,
|
|
20
|
+
password: boot.users.admin.applicationPassword
|
|
21
|
+
} }),
|
|
22
|
+
userId: boot.users.user.id,
|
|
23
|
+
adminId: boot.users.admin.id
|
|
24
|
+
};
|
|
25
|
+
for (const fixture of config.fixtures ?? []) if (fixture.seed) boot.fixtures[fixture.name] = await fixture.seed(ctx);
|
|
26
|
+
const path$1 = credentialsPath();
|
|
27
|
+
mkdirSync(dirname(path$1), { recursive: true });
|
|
28
|
+
writeFileSync(path$1, `${JSON.stringify({
|
|
29
|
+
url: boot.url,
|
|
30
|
+
users: boot.users,
|
|
31
|
+
fixtures: boot.fixtures
|
|
32
|
+
}, null, 2)}\n`);
|
|
33
|
+
return async () => {
|
|
34
|
+
for (const fixture of config.fixtures ?? []) await fixture.cleanup?.(ctx);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { runSeeds };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { WordPressService } from "../../wordpress/service.js";
|
|
2
|
+
import "../../wordpress/index.js";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/wp/types.d.ts
|
|
5
|
+
interface BaseTestUser {
|
|
6
|
+
id: number;
|
|
7
|
+
email: string;
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
}
|
|
13
|
+
interface TestAdminUser extends BaseTestUser {
|
|
14
|
+
applicationPassword: string;
|
|
15
|
+
}
|
|
16
|
+
interface TestUser extends BaseTestUser {
|
|
17
|
+
role: "subscriber";
|
|
18
|
+
}
|
|
19
|
+
/** Any JSON-serializable value — the artifact is written via `JSON.stringify`. */
|
|
20
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
21
|
+
[key: string]: JsonValue;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Shape of the credentials artifact written by `runSeeds` and consumed by
|
|
25
|
+
* `getKizloTestInstance`. The contract between the CLI writer and the test reader
|
|
26
|
+
* is this JSON file — neither side imports the other at runtime.
|
|
27
|
+
*/
|
|
28
|
+
interface TestCredentials {
|
|
29
|
+
url: string;
|
|
30
|
+
users: {
|
|
31
|
+
user: TestUser;
|
|
32
|
+
admin: TestAdminUser;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Per-fixture handles, keyed by `Fixture.name`. By convention each fixture
|
|
36
|
+
* stores the ids (or codes/slugs) of what it created — handles to re-fetch the
|
|
37
|
+
* live resource — not full snapshots, which would drift from WP state.
|
|
38
|
+
*/
|
|
39
|
+
fixtures: Record<string, Record<string, JsonValue>>;
|
|
40
|
+
}
|
|
41
|
+
/** Handed to each `Fixture` so it can create its world over REST (or wp-cli). */
|
|
42
|
+
interface SeedContext {
|
|
43
|
+
service: WordPressService;
|
|
44
|
+
adminId: number;
|
|
45
|
+
userId: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A plugin to ensure installed + active during bootstrap. A bare string is a
|
|
49
|
+
* wp.org slug (the installed name and the install source are the same). The
|
|
50
|
+
* object form is for any other source — a zip URL or a local path — where the
|
|
51
|
+
* installed slug (`name`, used for activation checks) can't be derived from the
|
|
52
|
+
* source. `source` is passed straight to `wp plugin install`.
|
|
53
|
+
*/
|
|
54
|
+
type PluginSource = string | {
|
|
55
|
+
name: string;
|
|
56
|
+
source: string;
|
|
57
|
+
};
|
|
58
|
+
/** The serializable test layer an extension ships alongside its live extension. */
|
|
59
|
+
interface Fixture {
|
|
60
|
+
/** Namespace key under `TestCredentials.fixtures`. */
|
|
61
|
+
name: string;
|
|
62
|
+
/** Plugins to install + activate via wp-cli during bootstrap, in order (deps first). */
|
|
63
|
+
plugins?: PluginSource[];
|
|
64
|
+
/** Build this extension's world once during seeding; return created handles. */
|
|
65
|
+
seed?: (ctx: SeedContext) => Promise<Record<string, JsonValue>>;
|
|
66
|
+
/** Revert per-test mutations (teardown); does NOT uninstall plugins or seeded data. */
|
|
67
|
+
cleanup?: (ctx: SeedContext) => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Identity helper for authoring a `Fixture` with full inference and autocomplete. */
|
|
70
|
+
declare function defineFixture(fixture: Fixture): Fixture;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { Fixture, JsonValue, PluginSource, SeedContext, TestAdminUser, TestCredentials, TestUser, defineFixture };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PluginSource, TestCredentials } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/wp/utils.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True only when the stack is already seeded: the credentials artifact exists on
|
|
7
|
+
* the host and this database carries the current seed marker (an option written
|
|
8
|
+
* as the final bootstrap step). The marker is independent of fixture content, so
|
|
9
|
+
* it stays accurate no matter which fixtures a consumer mounts; `option get` also
|
|
10
|
+
* fails when core isn't installed, so it doubles as the DB-initialized check.
|
|
11
|
+
*
|
|
12
|
+
* After `wp reset` the DB volume is gone so the marker is absent and `wp up`
|
|
13
|
+
* reseeds; after `wp stop` the DB is intact so it passes and `wp up` just resumes.
|
|
14
|
+
* Bumping `SEED_VERSION` invalidates older marks and forces a reseed.
|
|
15
|
+
*/
|
|
16
|
+
declare function isSeeded(): Promise<boolean>;
|
|
17
|
+
/** Build a GitHub release-zip URL for `PluginSource.source` (asset named `<tag>.zip`). */
|
|
18
|
+
declare function githubRelease(repo: string, tag: string): string;
|
|
19
|
+
/** Resolve a `PluginSource` to the `(name, source)` pair `ensurePlugin` needs. */
|
|
20
|
+
declare function resolvePluginSource(plugin: PluginSource): [name: string, source: string];
|
|
21
|
+
/**
|
|
22
|
+
* Ensure a plugin is active without ever re-downloading. Plugin files persist in
|
|
23
|
+
* the `wp_data` volume, so an already-installed plugin is just activated; the
|
|
24
|
+
* download (`source` = release zip URL or wp.org slug) happens only when the files
|
|
25
|
+
* are absent (i.e. after `wp reset` wipes the volume).
|
|
26
|
+
*/
|
|
27
|
+
declare function ensurePlugin(name: string, source: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* The directory holding `kizlo.config.*`, found by walking up from `cwd` (falling
|
|
30
|
+
* back to `cwd` when none is found). Anchoring the credentials artifact here —
|
|
31
|
+
* rather than each process's cwd — is what lets the CLI writer and the test reader
|
|
32
|
+
* agree even when tests run from a sub-directory of the config root.
|
|
33
|
+
*/
|
|
34
|
+
declare function findConfigDir(cwd?: string): string;
|
|
35
|
+
/** Fixed credentials artifact path, anchored to the config root. */
|
|
36
|
+
declare function credentialsPath(cwd?: string): string;
|
|
37
|
+
declare function getTestCredentials(): TestCredentials;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { credentialsPath, ensurePlugin, findConfigDir, getTestCredentials, githubRelease, isSeeded, resolvePluginSource };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CONFIG_FILES, CREDENTIALS_REL, SEED_MARKER_OPTION, SEED_VERSION } from "./constants.js";
|
|
2
|
+
import { compose, wpCli } from "./docker.js";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/wp/utils.ts
|
|
7
|
+
/**
|
|
8
|
+
* True only when the stack is already seeded: the credentials artifact exists on
|
|
9
|
+
* the host and this database carries the current seed marker (an option written
|
|
10
|
+
* as the final bootstrap step). The marker is independent of fixture content, so
|
|
11
|
+
* it stays accurate no matter which fixtures a consumer mounts; `option get` also
|
|
12
|
+
* fails when core isn't installed, so it doubles as the DB-initialized check.
|
|
13
|
+
*
|
|
14
|
+
* After `wp reset` the DB volume is gone so the marker is absent and `wp up`
|
|
15
|
+
* reseeds; after `wp stop` the DB is intact so it passes and `wp up` just resumes.
|
|
16
|
+
* Bumping `SEED_VERSION` invalidates older marks and forces a reseed.
|
|
17
|
+
*/
|
|
18
|
+
async function isSeeded() {
|
|
19
|
+
if (!existsSync(credentialsPath())) return false;
|
|
20
|
+
const marker = await compose([
|
|
21
|
+
"exec",
|
|
22
|
+
"-T",
|
|
23
|
+
"wp-cli",
|
|
24
|
+
"wp",
|
|
25
|
+
"option",
|
|
26
|
+
"get",
|
|
27
|
+
SEED_MARKER_OPTION
|
|
28
|
+
]);
|
|
29
|
+
return marker.code === 0 && marker.stdout.trim() === SEED_VERSION;
|
|
30
|
+
}
|
|
31
|
+
/** Build a GitHub release-zip URL for `PluginSource.source` (asset named `<tag>.zip`). */
|
|
32
|
+
function githubRelease(repo, tag) {
|
|
33
|
+
return `https://github.com/${repo}/releases/download/${tag}/${tag}.zip`;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve a `PluginSource` to the `(name, source)` pair `ensurePlugin` needs. */
|
|
36
|
+
function resolvePluginSource(plugin) {
|
|
37
|
+
return typeof plugin === "string" ? [plugin, plugin] : [plugin.name, plugin.source];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Ensure a plugin is active without ever re-downloading. Plugin files persist in
|
|
41
|
+
* the `wp_data` volume, so an already-installed plugin is just activated; the
|
|
42
|
+
* download (`source` = release zip URL or wp.org slug) happens only when the files
|
|
43
|
+
* are absent (i.e. after `wp reset` wipes the volume).
|
|
44
|
+
*/
|
|
45
|
+
async function ensurePlugin(name, source) {
|
|
46
|
+
if ((await compose([
|
|
47
|
+
"exec",
|
|
48
|
+
"-T",
|
|
49
|
+
"wp-cli",
|
|
50
|
+
"wp",
|
|
51
|
+
"plugin",
|
|
52
|
+
"is-active",
|
|
53
|
+
name
|
|
54
|
+
])).code === 0) return;
|
|
55
|
+
if ((await compose([
|
|
56
|
+
"exec",
|
|
57
|
+
"-T",
|
|
58
|
+
"wp-cli",
|
|
59
|
+
"wp",
|
|
60
|
+
"plugin",
|
|
61
|
+
"is-installed",
|
|
62
|
+
name
|
|
63
|
+
])).code === 0) {
|
|
64
|
+
await wpCli([
|
|
65
|
+
"plugin",
|
|
66
|
+
"activate",
|
|
67
|
+
name
|
|
68
|
+
]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await wpCli([
|
|
72
|
+
"plugin",
|
|
73
|
+
"install",
|
|
74
|
+
source,
|
|
75
|
+
"--activate"
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The directory holding `kizlo.config.*`, found by walking up from `cwd` (falling
|
|
80
|
+
* back to `cwd` when none is found). Anchoring the credentials artifact here —
|
|
81
|
+
* rather than each process's cwd — is what lets the CLI writer and the test reader
|
|
82
|
+
* agree even when tests run from a sub-directory of the config root.
|
|
83
|
+
*/
|
|
84
|
+
function findConfigDir(cwd = process.cwd()) {
|
|
85
|
+
let dir = resolve(cwd);
|
|
86
|
+
for (;;) {
|
|
87
|
+
if (CONFIG_FILES.some((name) => existsSync(resolve(dir, name)))) return dir;
|
|
88
|
+
const parent = dirname(dir);
|
|
89
|
+
if (parent === dir) return resolve(cwd);
|
|
90
|
+
dir = parent;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Fixed credentials artifact path, anchored to the config root. */
|
|
94
|
+
function credentialsPath(cwd) {
|
|
95
|
+
return resolve(findConfigDir(cwd), CREDENTIALS_REL);
|
|
96
|
+
}
|
|
97
|
+
let cached = null;
|
|
98
|
+
function getTestCredentials() {
|
|
99
|
+
if (cached) return cached;
|
|
100
|
+
cached = JSON.parse(readFileSync(credentialsPath(), "utf-8"));
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { credentialsPath, ensurePlugin, findConfigDir, getTestCredentials, githubRelease, isSeeded, resolvePluginSource };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResultClient } from "./shared/result.js";
|
|
2
|
+
import { AnyProcedureRouter, ExtractProcedureByScope } from "./shared/procedure.js";
|
|
3
|
+
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
interface KizloClientConfig<T extends AnyProcedureRouter> {
|
|
6
|
+
url?: string;
|
|
7
|
+
contract: T;
|
|
8
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
9
|
+
}
|
|
10
|
+
declare class KizloClient<TRouter extends AnyProcedureRouter> {
|
|
11
|
+
readonly client: ResultClient<ExtractProcedureByScope<TRouter, "remote" | "api">>;
|
|
12
|
+
protected readonly config: KizloClientConfig<TRouter>;
|
|
13
|
+
constructor(config: KizloClientConfig<TRouter>);
|
|
14
|
+
private getUrl;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a browser client for a generated contract. Defaults the URL to the
|
|
18
|
+
* current origin; framework packages wrap this to resolve it from their env.
|
|
19
|
+
*/
|
|
20
|
+
declare function createKizloClient<T extends AnyProcedureRouter>(contract: T, options?: {
|
|
21
|
+
url?: string;
|
|
22
|
+
}): KizloClient<T>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { KizloClient, KizloClientConfig, createKizloClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { RPC_PROTOCOL_HEADER } from "./shared/constants.js";
|
|
2
|
+
import { isContractProcedure as isContractProcedure$1, restoreContract } from "./shared/contract.js";
|
|
3
|
+
import { createResultClient } from "./shared/result.js";
|
|
4
|
+
import { getObjectProperty } from "./shared/utils.js";
|
|
5
|
+
import { DynamicLink, createORPCClient } from "@orpc/client";
|
|
6
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
7
|
+
import { inferRPCMethodFromContractRouter } from "@orpc/contract";
|
|
8
|
+
import { OpenAPILink } from "@orpc/openapi-client/fetch";
|
|
9
|
+
|
|
10
|
+
//#region src/client.ts
|
|
11
|
+
var KizloClient = class {
|
|
12
|
+
client;
|
|
13
|
+
config;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
const url = this.getUrl();
|
|
17
|
+
const orpcContract = restoreContract(config.contract);
|
|
18
|
+
const openapiLink = new OpenAPILink(orpcContract, {
|
|
19
|
+
url,
|
|
20
|
+
fetch: config.fetch
|
|
21
|
+
});
|
|
22
|
+
const remoteLink = new RPCLink({
|
|
23
|
+
url,
|
|
24
|
+
fetch: config.fetch,
|
|
25
|
+
headers: { [RPC_PROTOCOL_HEADER]: "1" },
|
|
26
|
+
method: inferRPCMethodFromContractRouter(orpcContract)
|
|
27
|
+
});
|
|
28
|
+
this.client = createResultClient(createORPCClient(new DynamicLink((_, path) => {
|
|
29
|
+
const procedure = getObjectProperty(this.config.contract, path);
|
|
30
|
+
if (!isContractProcedure$1(procedure)) throw new Error(`No valid procedure found at path "${path.join(".")}". This may happen when the contract router is not properly configured.`);
|
|
31
|
+
switch (procedure.scope) {
|
|
32
|
+
case "internal": throw new Error("Internal procedure can only be called on the server.");
|
|
33
|
+
case "api": return openapiLink;
|
|
34
|
+
case "remote": return remoteLink;
|
|
35
|
+
default: throw new Error();
|
|
36
|
+
}
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
getUrl() {
|
|
40
|
+
return this.config.url ?? window.location.href;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Creates a browser client for a generated contract. Defaults the URL to the
|
|
45
|
+
* current origin; framework packages wrap this to resolve it from their env.
|
|
46
|
+
*/
|
|
47
|
+
function createKizloClient(contract, options) {
|
|
48
|
+
return new KizloClient({
|
|
49
|
+
contract,
|
|
50
|
+
url: options?.url
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { KizloClient, createKizloClient };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineErrorMap } from "../shared/error.js";
|
|
2
|
+
|
|
3
|
+
//#region src/comment/errors.ts
|
|
4
|
+
const GET_COMMENT_ERROR_MAP = defineErrorMap({
|
|
5
|
+
COMMENT_NOT_FOUND: {
|
|
6
|
+
status: 404,
|
|
7
|
+
message: "Comment not found."
|
|
8
|
+
},
|
|
9
|
+
COMMENT_FORBIDDEN: {
|
|
10
|
+
status: 403,
|
|
11
|
+
message: "You are not allowed to read this comment."
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const LIST_COMMENT_ERROR_MAP = defineErrorMap({
|
|
15
|
+
COMMENT_FORBIDDEN: {
|
|
16
|
+
status: 403,
|
|
17
|
+
message: "You are not allowed to read comments."
|
|
18
|
+
},
|
|
19
|
+
COMMENT_POST_TYPE_NOT_SUPPORTED: {
|
|
20
|
+
status: 400,
|
|
21
|
+
message: "The post type does not support comments."
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const SUBMIT_COMMENT_ERROR_MAP = defineErrorMap({
|
|
25
|
+
COMMENT_USER_NOT_FOUND: {},
|
|
26
|
+
COMMENT_ID_NOT_FOUND: {},
|
|
27
|
+
COMMENT_CLOSED: {},
|
|
28
|
+
COMMENT_LOGIN_REQUIRED: {},
|
|
29
|
+
COMMENT_NAME_EMAIL_REQUIRED: {},
|
|
30
|
+
COMMENT_INVALID_EMAIL: {},
|
|
31
|
+
COMMENT_EMPTY_CONTENT: {},
|
|
32
|
+
COMMENT_FIELD_TOO_LONG: {},
|
|
33
|
+
COMMENT_DUPLICATE: {},
|
|
34
|
+
COMMENT_RATE_LIMITED: {},
|
|
35
|
+
COMMENT_PARENT_UNAPPROVED: {},
|
|
36
|
+
COMMENT_CAPTCHA_REQUIRED: {
|
|
37
|
+
status: 400,
|
|
38
|
+
message: "Comment captcha is required."
|
|
39
|
+
},
|
|
40
|
+
COMMENT_CAPTCHA_INVALID: {
|
|
41
|
+
status: 400,
|
|
42
|
+
message: "Comment captcha is invalid."
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { GET_COMMENT_ERROR_MAP, LIST_COMMENT_ERROR_MAP, SUBMIT_COMMENT_ERROR_MAP };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createProcedure } from "../shared/procedure.js";
|
|
2
|
+
import { WP_CORE_BASE, WP_KIZLO_BASE } from "../wordpress/constants.js";
|
|
3
|
+
import { deserializeListMetadata } from "../shared/serialize.js";
|
|
4
|
+
import { GET_COMMENT_ERROR_MAP, LIST_COMMENT_ERROR_MAP, SUBMIT_COMMENT_ERROR_MAP } from "./errors.js";
|
|
5
|
+
import { Comment, CommentList, GetCommentInput, ListCommentInput, SubmitCommentInput } from "./schema.js";
|
|
6
|
+
import { deserializeComment } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/comment/index.ts
|
|
9
|
+
const COMMENT_ROUTER_MAP = {
|
|
10
|
+
get: createProcedure({
|
|
11
|
+
scope: "api",
|
|
12
|
+
method: "GET",
|
|
13
|
+
path: "/comments/{id}",
|
|
14
|
+
params: GetCommentInput.pick({ id: true }),
|
|
15
|
+
query: GetCommentInput.pick({ password: true }).optional(),
|
|
16
|
+
errors: GET_COMMENT_ERROR_MAP,
|
|
17
|
+
output: Comment
|
|
18
|
+
}, async ({ context, errors, input }) => {
|
|
19
|
+
const response = await context.service.wordpress.get(`/comments/${input.params.id}`, {
|
|
20
|
+
base: WP_CORE_BASE,
|
|
21
|
+
searchParams: { password: input.query?.password }
|
|
22
|
+
});
|
|
23
|
+
if (response.error) switch (response.error.code) {
|
|
24
|
+
case "rest_comment_invalid_id":
|
|
25
|
+
case "rest_post_invalid_id": throw errors.COMMENT_NOT_FOUND();
|
|
26
|
+
case "rest_cannot_read":
|
|
27
|
+
case "rest_cannot_read_post": throw errors.COMMENT_FORBIDDEN();
|
|
28
|
+
default:
|
|
29
|
+
context.logger.error("Get comment unhandled error", response.error, {
|
|
30
|
+
commentId: input.params.id,
|
|
31
|
+
code: response.error.code
|
|
32
|
+
});
|
|
33
|
+
throw errors.INTERNAL_SERVER_ERROR();
|
|
34
|
+
}
|
|
35
|
+
return deserializeComment(response.data);
|
|
36
|
+
}),
|
|
37
|
+
list: createProcedure({
|
|
38
|
+
scope: "api",
|
|
39
|
+
method: "GET",
|
|
40
|
+
path: "/comments",
|
|
41
|
+
query: ListCommentInput.optional(),
|
|
42
|
+
errors: LIST_COMMENT_ERROR_MAP,
|
|
43
|
+
output: CommentList
|
|
44
|
+
}, async ({ context, errors, input }) => {
|
|
45
|
+
const response = await context.service.wordpress.get(`/comments`, {
|
|
46
|
+
base: WP_CORE_BASE,
|
|
47
|
+
searchParams: {
|
|
48
|
+
context: "edit",
|
|
49
|
+
after: input.query?.after,
|
|
50
|
+
author: input.query?.author,
|
|
51
|
+
author_exclude: input.query?.authorExclude,
|
|
52
|
+
before: input.query?.before,
|
|
53
|
+
exclude: input.query?.exclude,
|
|
54
|
+
include: input.query?.include,
|
|
55
|
+
offset: input.query?.offset,
|
|
56
|
+
order: input.query?.order,
|
|
57
|
+
orderby: input.query?.orderby,
|
|
58
|
+
page: input.query?.page,
|
|
59
|
+
parent: input.query?.parent,
|
|
60
|
+
parent_exclude: input.query?.parentExclude,
|
|
61
|
+
password: input.query?.password,
|
|
62
|
+
per_page: input.query?.perPage,
|
|
63
|
+
post: input.query?.post,
|
|
64
|
+
search: input.query?.search
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (response.error) switch (response.error.code) {
|
|
68
|
+
case "rest_cannot_read":
|
|
69
|
+
case "rest_cannot_read_post":
|
|
70
|
+
case "rest_forbidden_context":
|
|
71
|
+
case "rest_forbidden_param": throw errors.COMMENT_FORBIDDEN();
|
|
72
|
+
case "rest_comment_not_supported_post_type": throw errors.COMMENT_POST_TYPE_NOT_SUPPORTED();
|
|
73
|
+
default:
|
|
74
|
+
context.logger.error("List comments unhandled error", response.error, { code: response.error.code });
|
|
75
|
+
throw errors.INTERNAL_SERVER_ERROR();
|
|
76
|
+
}
|
|
77
|
+
const list = context.service.wordpress.resolveList({
|
|
78
|
+
data: response.data,
|
|
79
|
+
headers: response.headers,
|
|
80
|
+
searchParams: input.query
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
items: list.items.map(deserializeComment),
|
|
84
|
+
meta: deserializeListMetadata(list.meta)
|
|
85
|
+
};
|
|
86
|
+
}),
|
|
87
|
+
submit: createProcedure({
|
|
88
|
+
scope: "api",
|
|
89
|
+
method: "POST",
|
|
90
|
+
path: "/comments",
|
|
91
|
+
body: SubmitCommentInput,
|
|
92
|
+
errors: SUBMIT_COMMENT_ERROR_MAP,
|
|
93
|
+
output: Comment
|
|
94
|
+
}, async ({ context, errors, input }) => {
|
|
95
|
+
const connInfo = await context.getConnInfo();
|
|
96
|
+
if (!connInfo?.ip) throw new Error("Connection IP is required.");
|
|
97
|
+
if (!connInfo?.userAgent) throw new Error("Connection user agent is required.");
|
|
98
|
+
const user = await context.getAuthUser();
|
|
99
|
+
if (!user) {
|
|
100
|
+
if (!input.body.captchaToken) throw errors.COMMENT_CAPTCHA_REQUIRED();
|
|
101
|
+
if (!await context.verifyCaptcha(input.body.captchaToken)) throw errors.COMMENT_CAPTCHA_INVALID();
|
|
102
|
+
}
|
|
103
|
+
const response = await context.service.wordpress.post(`/comments`, {
|
|
104
|
+
body: {
|
|
105
|
+
user_id: user?.id,
|
|
106
|
+
author_ip: connInfo.ip,
|
|
107
|
+
user_agent: connInfo.userAgent,
|
|
108
|
+
content: input.body.content,
|
|
109
|
+
post_id: input.body.postId,
|
|
110
|
+
author_email: input.body.authorEmail,
|
|
111
|
+
author_name: input.body.authorName,
|
|
112
|
+
author_url: input.body.authorUrl,
|
|
113
|
+
parent: input.body.parentId
|
|
114
|
+
},
|
|
115
|
+
base: WP_KIZLO_BASE
|
|
116
|
+
});
|
|
117
|
+
if (response.error) switch (response.error.code) {
|
|
118
|
+
case "rest_comment_author_data_required": throw errors.COMMENT_NAME_EMAIL_REQUIRED();
|
|
119
|
+
case "rest_comment_author_invalid":
|
|
120
|
+
case "rest_comment_invalid_author": throw errors.COMMENT_USER_NOT_FOUND();
|
|
121
|
+
case "rest_comment_content_invalid": throw errors.COMMENT_EMPTY_CONTENT();
|
|
122
|
+
case "rest_comment_login_required": throw errors.COMMENT_LOGIN_REQUIRED();
|
|
123
|
+
case "rest_comment_closed": throw errors.COMMENT_CLOSED();
|
|
124
|
+
case "rest_cannot_read_post":
|
|
125
|
+
case "rest_comment_draft_post":
|
|
126
|
+
case "rest_comment_invalid_post_id":
|
|
127
|
+
case "rest_comment_not_supported_post_type":
|
|
128
|
+
case "rest_comment_trash_post": throw errors.COMMENT_ID_NOT_FOUND();
|
|
129
|
+
default:
|
|
130
|
+
context.logger.error("Submit comment unhandled error", response.error, { code: response.error.code });
|
|
131
|
+
throw errors.INTERNAL_SERVER_ERROR();
|
|
132
|
+
}
|
|
133
|
+
return deserializeComment(response.data);
|
|
134
|
+
})
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
export { COMMENT_ROUTER_MAP };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ListMetadata, ListOrder, Media } from "../shared/schema.js";
|
|
2
|
+
import { NumberLike, arrayable } from "@kizlo/shared";
|
|
3
|
+
import z from "zod/v4";
|
|
4
|
+
|
|
5
|
+
//#region src/comment/schema.ts
|
|
6
|
+
const CommentAuthorRef = z.object({
|
|
7
|
+
id: z.number(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
avatar: Media.nullable()
|
|
10
|
+
});
|
|
11
|
+
const CommentPostRef = z.object({
|
|
12
|
+
id: z.number(),
|
|
13
|
+
slug: z.string(),
|
|
14
|
+
title: z.string(),
|
|
15
|
+
image: Media.nullable()
|
|
16
|
+
});
|
|
17
|
+
const Comment = z.object({
|
|
18
|
+
id: z.number(),
|
|
19
|
+
parentId: z.number(),
|
|
20
|
+
author: CommentAuthorRef.nullable(),
|
|
21
|
+
content: z.string(),
|
|
22
|
+
post: CommentPostRef,
|
|
23
|
+
replyCount: z.number(),
|
|
24
|
+
postedAt: z.number(),
|
|
25
|
+
isApproved: z.boolean()
|
|
26
|
+
});
|
|
27
|
+
const GetCommentInput = z.object({
|
|
28
|
+
id: NumberLike,
|
|
29
|
+
password: z.string().optional()
|
|
30
|
+
});
|
|
31
|
+
const COMMENT_LIST_ORDER_BYS = [
|
|
32
|
+
"date",
|
|
33
|
+
"id",
|
|
34
|
+
"include",
|
|
35
|
+
"post",
|
|
36
|
+
"parent"
|
|
37
|
+
];
|
|
38
|
+
const CommentListOrderBy = z.enum(COMMENT_LIST_ORDER_BYS);
|
|
39
|
+
const ListCommentInput = z.object({
|
|
40
|
+
page: NumberLike.optional(),
|
|
41
|
+
perPage: NumberLike.optional(),
|
|
42
|
+
search: z.string().optional(),
|
|
43
|
+
after: z.iso.datetime().optional(),
|
|
44
|
+
author: arrayable(NumberLike).optional(),
|
|
45
|
+
authorExclude: arrayable(NumberLike).optional(),
|
|
46
|
+
before: z.iso.datetime().optional(),
|
|
47
|
+
exclude: arrayable(NumberLike).optional(),
|
|
48
|
+
include: arrayable(NumberLike).optional(),
|
|
49
|
+
offset: NumberLike.optional(),
|
|
50
|
+
order: ListOrder.optional(),
|
|
51
|
+
orderby: CommentListOrderBy.optional(),
|
|
52
|
+
parent: arrayable(NumberLike).optional(),
|
|
53
|
+
parentExclude: arrayable(NumberLike).optional(),
|
|
54
|
+
post: arrayable(NumberLike).optional(),
|
|
55
|
+
password: z.string().optional()
|
|
56
|
+
});
|
|
57
|
+
const CommentList = z.object({
|
|
58
|
+
items: z.array(Comment),
|
|
59
|
+
meta: ListMetadata
|
|
60
|
+
});
|
|
61
|
+
const SubmitCommentInput = z.object({
|
|
62
|
+
postId: z.number(),
|
|
63
|
+
parentId: z.number().optional(),
|
|
64
|
+
content: z.string(),
|
|
65
|
+
authorEmail: z.email().optional(),
|
|
66
|
+
authorName: z.string().optional(),
|
|
67
|
+
authorUrl: z.url().optional(),
|
|
68
|
+
captchaToken: z.string().optional()
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { Comment, CommentList, GetCommentInput, ListCommentInput, SubmitCommentInput };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/comment/utils.ts
|
|
2
|
+
function deserializeComment(data) {
|
|
3
|
+
return {
|
|
4
|
+
id: data.id,
|
|
5
|
+
author: data.kizlo.author ? {
|
|
6
|
+
id: data.kizlo.author.id,
|
|
7
|
+
avatar: data.kizlo.author.avatar_url ? {
|
|
8
|
+
id: 0,
|
|
9
|
+
alt: "",
|
|
10
|
+
name: "",
|
|
11
|
+
src: data.kizlo.author.avatar_url
|
|
12
|
+
} : null,
|
|
13
|
+
name: data.kizlo.author.name
|
|
14
|
+
} : null,
|
|
15
|
+
content: data.content.rendered,
|
|
16
|
+
isApproved: data.status === "approved",
|
|
17
|
+
parentId: data.parent,
|
|
18
|
+
post: {
|
|
19
|
+
id: data.kizlo.post.id,
|
|
20
|
+
image: data.kizlo.post.featured_image ? {
|
|
21
|
+
id: data.kizlo.post.featured_image.id,
|
|
22
|
+
alt: data.kizlo.post.featured_image.alt,
|
|
23
|
+
src: data.kizlo.post.featured_image.url,
|
|
24
|
+
name: ""
|
|
25
|
+
} : null,
|
|
26
|
+
title: data.kizlo.post.title,
|
|
27
|
+
slug: data.kizlo.post.slug
|
|
28
|
+
},
|
|
29
|
+
postedAt: new Date(data.date).getTime(),
|
|
30
|
+
replyCount: data.kizlo.reply_count
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { deserializeComment };
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Fixture } from "./cli/wp/types.js";
|
|
2
|
+
import "./test.js";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
interface KizloTestConfig {
|
|
6
|
+
/** Published WP port (default 8080). */
|
|
7
|
+
port?: number;
|
|
8
|
+
/** Extension fixtures to install + seed. */
|
|
9
|
+
fixtures?: Fixture[];
|
|
10
|
+
/**
|
|
11
|
+
* Package manager used to launch the test script (default: auto-detected from
|
|
12
|
+
* the lockfile / package.json `packageManager`).
|
|
13
|
+
*/
|
|
14
|
+
packageManager?: "npm" | "pnpm" | "yarn" | "bun";
|
|
15
|
+
/**
|
|
16
|
+
* Override the test command entirely. Default: `<packageManager> test` (the
|
|
17
|
+
* project's own `test` script — never hardcoded to vitest).
|
|
18
|
+
*/
|
|
19
|
+
command?: string;
|
|
20
|
+
}
|
|
21
|
+
interface KizloGlobalConfig {
|
|
22
|
+
/**
|
|
23
|
+
* Kizlo's home directory. Kizlo owns the layout inside it: your server
|
|
24
|
+
* (`server/`, whose `index.ts` exports `router` plus your extensions), the
|
|
25
|
+
* browser `client.ts`, and the generated contract.
|
|
26
|
+
* @default 'src/lib/kizlo' (or 'lib/kizlo' without a src dir)
|
|
27
|
+
*/
|
|
28
|
+
dir?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Import alias prefix for generated imports (e.g. `@`). Detected from
|
|
31
|
+
* tsconfig at init; omit (or empty) to use relative imports.
|
|
32
|
+
*/
|
|
33
|
+
alias?: string;
|
|
34
|
+
/** WordPress test environment used by `kizlo wp` and `kizlo test`. */
|
|
35
|
+
test?: KizloTestConfig;
|
|
36
|
+
}
|
|
37
|
+
declare function defineConfig(config: KizloGlobalConfig): KizloGlobalConfig;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { KizloGlobalConfig, KizloTestConfig, defineConfig };
|