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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/adapters/auth.d.ts +18 -0
  4. package/dist/adapters/auth.js +15 -0
  5. package/dist/adapters/captcha.d.ts +28 -0
  6. package/dist/adapters/captcha.js +86 -0
  7. package/dist/adapters/geo.d.ts +18 -0
  8. package/dist/adapters/geo.js +18 -0
  9. package/dist/adapters/geo.vercel.d.ts +6 -0
  10. package/dist/adapters/geo.vercel.js +41 -0
  11. package/dist/adapters/logger.d.ts +31 -0
  12. package/dist/adapters/logger.js +30 -0
  13. package/dist/cli/banner.js +26 -0
  14. package/dist/cli/commands/generate.js +34 -0
  15. package/dist/cli/commands/init.js +313 -0
  16. package/dist/cli/commands/test.js +68 -0
  17. package/dist/cli/commands/watch.js +53 -0
  18. package/dist/cli/commands/wp.js +66 -0
  19. package/dist/cli/daemon/config.js +87 -0
  20. package/dist/cli/daemon/generate.js +40 -0
  21. package/dist/cli/daemon/lock.js +31 -0
  22. package/dist/cli/daemon/logger.js +8 -0
  23. package/dist/cli/daemon/watch.js +37 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +22 -0
  26. package/dist/cli/presets/base.js +23 -0
  27. package/dist/cli/presets/index.js +16 -0
  28. package/dist/cli/presets/nextjs.js +39 -0
  29. package/dist/cli/utils.js +182 -0
  30. package/dist/cli/wp/bootstrap.js +114 -0
  31. package/dist/cli/wp/compose/docker-compose.yml +73 -0
  32. package/dist/cli/wp/constants.js +52 -0
  33. package/dist/cli/wp/docker.d.ts +24 -0
  34. package/dist/cli/wp/docker.js +69 -0
  35. package/dist/cli/wp/setup.js +39 -0
  36. package/dist/cli/wp/types.d.ts +72 -0
  37. package/dist/cli/wp/types.js +8 -0
  38. package/dist/cli/wp/utils.d.ts +39 -0
  39. package/dist/cli/wp/utils.js +105 -0
  40. package/dist/client.d.ts +24 -0
  41. package/dist/client.js +55 -0
  42. package/dist/comment/errors.js +47 -0
  43. package/dist/comment/index.js +138 -0
  44. package/dist/comment/schema.js +72 -0
  45. package/dist/comment/utils.js +35 -0
  46. package/dist/config.d.ts +39 -0
  47. package/dist/config.js +7 -0
  48. package/dist/context.d.ts +73 -0
  49. package/dist/context.js +145 -0
  50. package/dist/cookie.d.ts +19 -0
  51. package/dist/cookie.js +25 -0
  52. package/dist/email/service.d.ts +11 -0
  53. package/dist/email/service.interface.d.ts +8 -0
  54. package/dist/email/service.js +20 -0
  55. package/dist/index.d.ts +39 -0
  56. package/dist/index.js +31 -0
  57. package/dist/integrations/nextjs/kizlo.client.d.ts +10 -0
  58. package/dist/integrations/nextjs/kizlo.client.js +13 -0
  59. package/dist/integrations/nextjs/kizlo.server.d.ts +12 -0
  60. package/dist/integrations/nextjs/kizlo.server.js +13 -0
  61. package/dist/integrations/nextjs/utils.js +11 -0
  62. package/dist/kizlo.d.ts +73 -0
  63. package/dist/kizlo.js +144 -0
  64. package/dist/menu/errors.js +16 -0
  65. package/dist/menu/fixture.d.ts +8 -0
  66. package/dist/menu/fixture.js +62 -0
  67. package/dist/menu/index.js +70 -0
  68. package/dist/menu/schema.js +70 -0
  69. package/dist/menu/utils.js +57 -0
  70. package/dist/nextjs/server.d.ts +2 -0
  71. package/dist/nextjs/server.js +3 -0
  72. package/dist/nextjs.d.ts +2 -0
  73. package/dist/nextjs.js +3 -0
  74. package/dist/package.js +5 -0
  75. package/dist/post/error.js +30 -0
  76. package/dist/post/fixture.d.ts +8 -0
  77. package/dist/post/fixture.js +71 -0
  78. package/dist/post/index.js +123 -0
  79. package/dist/post/schema.js +120 -0
  80. package/dist/post/utils.js +42 -0
  81. package/dist/post-type/service.js +48 -0
  82. package/dist/router.d.ts +624 -0
  83. package/dist/router.js +15 -0
  84. package/dist/seo/index.js +54 -0
  85. package/dist/seo/schema.d.ts +196 -0
  86. package/dist/seo/schema.js +96 -0
  87. package/dist/seo/types.d.ts +80 -0
  88. package/dist/seo/utils.js +44 -0
  89. package/dist/service.d.ts +17 -0
  90. package/dist/service.js +17 -0
  91. package/dist/shared/constants.js +10 -0
  92. package/dist/shared/contract.d.ts +21 -0
  93. package/dist/shared/contract.js +43 -0
  94. package/dist/shared/crypto.d.ts +28 -0
  95. package/dist/shared/crypto.js +34 -0
  96. package/dist/shared/error.d.ts +128 -0
  97. package/dist/shared/error.js +147 -0
  98. package/dist/shared/extension.d.ts +25 -0
  99. package/dist/shared/extension.js +7 -0
  100. package/dist/shared/identifier.d.ts +11 -0
  101. package/dist/shared/identifier.js +26 -0
  102. package/dist/shared/middleware.d.ts +29 -0
  103. package/dist/shared/procedure.d.ts +114 -0
  104. package/dist/shared/procedure.js +114 -0
  105. package/dist/shared/result.d.ts +24 -0
  106. package/dist/shared/result.js +49 -0
  107. package/dist/shared/router.d.ts +7 -0
  108. package/dist/shared/router.js +26 -0
  109. package/dist/shared/schema.d.ts +55 -0
  110. package/dist/shared/schema.js +43 -0
  111. package/dist/shared/serialize.d.ts +12 -0
  112. package/dist/shared/serialize.js +51 -0
  113. package/dist/shared/types.d.ts +35 -0
  114. package/dist/shared/utils.js +17 -0
  115. package/dist/test/auth.d.ts +7 -0
  116. package/dist/test/auth.js +15 -0
  117. package/dist/test/captcha.d.ts +7 -0
  118. package/dist/test/captcha.js +8 -0
  119. package/dist/test/client.d.ts +11 -0
  120. package/dist/test/client.js +14 -0
  121. package/dist/test/fixtures.d.ts +14 -0
  122. package/dist/test/fixtures.js +13 -0
  123. package/dist/test/server.d.ts +10 -0
  124. package/dist/test/server.js +32 -0
  125. package/dist/test/users.d.ts +14 -0
  126. package/dist/test/users.js +17 -0
  127. package/dist/test.d.ts +12 -0
  128. package/dist/test.js +13 -0
  129. package/dist/webhook/index.d.ts +38 -0
  130. package/dist/webhook/index.js +44 -0
  131. package/dist/webhook/schema.d.ts +45 -0
  132. package/dist/webhook/schema.js +72 -0
  133. package/dist/webhook/utils.js +21 -0
  134. package/dist/wordpress/comment/service.d.ts +28 -0
  135. package/dist/wordpress/comment/service.js +55 -0
  136. package/dist/wordpress/comment/types.d.ts +206 -0
  137. package/dist/wordpress/constants.d.ts +14 -0
  138. package/dist/wordpress/constants.js +13 -0
  139. package/dist/wordpress/error.d.ts +9 -0
  140. package/dist/wordpress/error.js +12 -0
  141. package/dist/wordpress/index.d.ts +15 -0
  142. package/dist/wordpress/menu/items/service.d.ts +28 -0
  143. package/dist/wordpress/menu/items/service.js +55 -0
  144. package/dist/wordpress/menu/items/types.d.ts +214 -0
  145. package/dist/wordpress/menu/service.d.ts +30 -0
  146. package/dist/wordpress/menu/service.js +58 -0
  147. package/dist/wordpress/menu/types.d.ts +115 -0
  148. package/dist/wordpress/post/service.d.ts +28 -0
  149. package/dist/wordpress/post/service.js +55 -0
  150. package/dist/wordpress/post/types.d.ts +290 -0
  151. package/dist/wordpress/service.d.ts +77 -0
  152. package/dist/wordpress/service.js +180 -0
  153. package/dist/wordpress/types.d.ts +67 -0
  154. package/dist/wordpress/user/service.d.ts +28 -0
  155. package/dist/wordpress/user/service.js +55 -0
  156. package/dist/wordpress/user/types.d.ts +221 -0
  157. package/dist/wordpress/utils.d.ts +7 -0
  158. package/dist/wordpress/utils.js +9 -0
  159. 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,8 @@
1
+ //#region src/cli/wp/types.ts
2
+ /** Identity helper for authoring a `Fixture` with full inference and autocomplete. */
3
+ function defineFixture(fixture) {
4
+ return fixture;
5
+ }
6
+
7
+ //#endregion
8
+ export { 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 };
@@ -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 };
@@ -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 };