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
package/dist/kizlo.js ADDED
@@ -0,0 +1,144 @@
1
+ import { RPC_PROTOCOL_HEADER } from "./shared/constants.js";
2
+ import { KizloError } from "./shared/error.js";
3
+ import { createOrpcRouter } from "./shared/router.js";
4
+ import { createResultClient } from "./shared/result.js";
5
+ import { isTypescriptObject } from "./shared/utils.js";
6
+ import { Context } from "./context.js";
7
+ import { ROUTER_MAP } from "./router.js";
8
+ import { createWebhookRouter } from "./webhook/index.js";
9
+ import { tryCatch } from "@kizlo/shared";
10
+ import { ORPCError, createRouterClient } from "@orpc/server";
11
+ import { OpenAPIHandler } from "@orpc/openapi/fetch";
12
+ import { RPCHandler } from "@orpc/server/fetch";
13
+
14
+ //#region src/kizlo.ts
15
+ var Kizlo = class {
16
+ context;
17
+ client;
18
+ router;
19
+ remoteHandler;
20
+ openapiHandler;
21
+ config;
22
+ constructor(config) {
23
+ this.config = config;
24
+ const extensions = this.registerExtensions();
25
+ this.context = new Context({
26
+ adapters: config.adapters,
27
+ siteSecret: config.siteSecret,
28
+ credentials: config.credentials
29
+ });
30
+ this.router = Object.assign(Object.assign({}, {
31
+ ...extensions.router,
32
+ ...ROUTER_MAP
33
+ }), { webhooks: createWebhookRouter({
34
+ path: this.config.webhookPath,
35
+ events: extensions.events
36
+ }) });
37
+ const orpcRouter = createOrpcRouter(this.router);
38
+ this.client = createResultClient(createRouterClient(orpcRouter, { context: this.context }));
39
+ this.remoteHandler = new RPCHandler(orpcRouter, {
40
+ filter: (options) => {
41
+ return this.checkInvocationScope(options.contract["~orpc"].meta, "remote");
42
+ },
43
+ interceptors: [this.errorInterceptor()]
44
+ });
45
+ this.openapiHandler = new OpenAPIHandler(orpcRouter, {
46
+ filter: (options) => {
47
+ return this.checkInvocationScope(options.contract["~orpc"].meta, "api");
48
+ },
49
+ interceptors: [this.errorInterceptor()],
50
+ customErrorResponseBodyEncoder(error) {
51
+ const { defined,...rest } = error.toJSON();
52
+ return rest;
53
+ }
54
+ });
55
+ this.handler = this.handler.bind(this);
56
+ }
57
+ async handler(request) {
58
+ const serverUrl = new URL(this.config.baseUrl);
59
+ const context = this.context.createRestContext(request);
60
+ const result = !(request.headers.get(RPC_PROTOCOL_HEADER) !== null) ? await this.openapiHandler.handle(request, {
61
+ context,
62
+ prefix: serverUrl.pathname
63
+ }) : await this.remoteHandler.handle(request, {
64
+ context,
65
+ prefix: serverUrl.pathname
66
+ });
67
+ if (!result.matched) return new Response("Not Found", { status: 404 });
68
+ for (const [key, value] of context.headers.entries()) if (key.toLowerCase() === "set-cookie") context.headers.getSetCookie().forEach((cookie) => {
69
+ result.response.headers.append("Set-Cookie", cookie);
70
+ });
71
+ else result.response.headers.set(key, value);
72
+ return result.response;
73
+ }
74
+ errorInterceptor() {
75
+ return async ({ next }) => {
76
+ const [err, data] = await tryCatch(next());
77
+ if (err) {
78
+ if (err instanceof KizloError) throw new ORPCError(err.code, {
79
+ status: err.status,
80
+ message: err.message,
81
+ data: err.data
82
+ });
83
+ console.log(err);
84
+ throw err;
85
+ }
86
+ return data;
87
+ };
88
+ }
89
+ checkInvocationScope(value, scope) {
90
+ return isTypescriptObject(value) && "scope" in value && value.scope === scope;
91
+ }
92
+ registerExtensions() {
93
+ const router = {};
94
+ const events = [];
95
+ for (const extension of this.config.extensions ?? []) {
96
+ const data = extension.init({ context: { something: "" } });
97
+ if (data.router) router[extension.id] = data.router;
98
+ for (const handler of data.events ?? []) events.push(handler);
99
+ }
100
+ return {
101
+ events,
102
+ router
103
+ };
104
+ }
105
+ };
106
+ function requireEnv(name) {
107
+ const value = process.env[name]?.trim();
108
+ if (!value) throw new KizloError("MISSING_ENV_VARIABLE", { message: `Please define ${name} in your .env file.` });
109
+ return value;
110
+ }
111
+ /**
112
+ * Resolves a full `KizloConfig` from options and the environment. Shared by the
113
+ * base `createKizlo` and each framework factory — the single place env values
114
+ * and credentials are read. `baseUrlEnvKey` is the only framework-varying part.
115
+ */
116
+ function resolveKizloConfig(options, defaults) {
117
+ const credentials = options?.wordpress?.credentials;
118
+ return {
119
+ baseUrl: options?.baseUrl ?? requireEnv(defaults.baseUrlEnvKey),
120
+ siteSecret: options?.siteSecret ?? requireEnv("SITE_SECRET"),
121
+ environment: options?.environment ?? process.env.NODE_ENV ?? "development",
122
+ extensions: options?.extensions,
123
+ adapters: {
124
+ ...defaults.adapters,
125
+ ...options?.adapters
126
+ },
127
+ credentials: {
128
+ url: credentials?.url ?? requireEnv("WORDPRESS_URL"),
129
+ username: credentials?.username ?? requireEnv("WORDPRESS_USERNAME"),
130
+ password: credentials?.password ?? requireEnv("WORDPRESS_APPLICATION_PASSWORD")
131
+ }
132
+ };
133
+ }
134
+ /**
135
+ * Creates a Kizlo server from the environment (`SERVER_BASE_URL`, `SITE_SECRET`,
136
+ * `WORDPRESS_*`), with options taking precedence. Framework packages wrap this
137
+ * with their own URL convention and adapters.
138
+ */
139
+ function createKizlo(options) {
140
+ return new Kizlo(resolveKizloConfig(options, { baseUrlEnvKey: "SERVER_BASE_URL" }));
141
+ }
142
+
143
+ //#endregion
144
+ export { Kizlo, createKizlo, resolveKizloConfig };
@@ -0,0 +1,16 @@
1
+ import { defineErrorMap } from "../shared/error.js";
2
+
3
+ //#region src/menu/errors.ts
4
+ const LIST_MENU_ITEM_ERROR_MAP = defineErrorMap({
5
+ MENU_ITEM_INVALID_PAGE: {
6
+ status: 400,
7
+ message: "The requested page does not exist."
8
+ },
9
+ MENU_ITEM_FORBIDDEN: {
10
+ status: 403,
11
+ message: "You are not allowed to view menu items."
12
+ }
13
+ });
14
+
15
+ //#endregion
16
+ export { LIST_MENU_ITEM_ERROR_MAP };
@@ -0,0 +1,8 @@
1
+ import { Fixture } from "../cli/wp/types.js";
2
+ import "../test.js";
3
+
4
+ //#region src/menu/fixture.d.ts
5
+ /** Core menu fixture: seeds a `primary` nav menu + items so the menu suite has content to read. */
6
+ declare function menuFixture(): Fixture;
7
+ //#endregion
8
+ export { menuFixture };
@@ -0,0 +1,62 @@
1
+ import { defineFixture } from "../cli/wp/types.js";
2
+
3
+ //#region src/menu/fixture.ts
4
+ const MENU = {
5
+ name: "Primary",
6
+ slug: "primary",
7
+ items: [
8
+ {
9
+ label: "Home",
10
+ url: "/"
11
+ },
12
+ {
13
+ label: "About",
14
+ url: "/about"
15
+ },
16
+ {
17
+ label: "Contact",
18
+ url: "/contact"
19
+ }
20
+ ]
21
+ };
22
+ async function upsertMenu(ctx) {
23
+ const existing = await ctx.service.menus.list({ per_page: 100 });
24
+ if (existing.error) throw existing.error;
25
+ const found = existing.data.items.find((menu) => menu.slug === MENU.slug);
26
+ if (found) return found.id;
27
+ const created = await ctx.service.menus.create({ name: MENU.name });
28
+ if (created.error) throw created.error;
29
+ return created.data.id;
30
+ }
31
+ async function seedItems(ctx, menuId) {
32
+ const existing = await ctx.service.menus.items.list({
33
+ menus: menuId,
34
+ per_page: 1
35
+ });
36
+ if (existing.error) throw existing.error;
37
+ if (existing.data.items.length > 0) return;
38
+ for (const item of MENU.items) {
39
+ const created = await ctx.service.menus.items.create({
40
+ menus: menuId,
41
+ title: item.label,
42
+ url: item.url,
43
+ type: "custom",
44
+ status: "publish"
45
+ });
46
+ if (created.error) throw created.error;
47
+ }
48
+ }
49
+ /** Core menu fixture: seeds a `primary` nav menu + items so the menu suite has content to read. */
50
+ function menuFixture() {
51
+ return defineFixture({
52
+ name: "menu",
53
+ async seed(ctx) {
54
+ const menuId = await upsertMenu(ctx);
55
+ await seedItems(ctx, menuId);
56
+ return { menuId };
57
+ }
58
+ });
59
+ }
60
+
61
+ //#endregion
62
+ export { menuFixture };
@@ -0,0 +1,70 @@
1
+ import { createProcedure } from "../shared/procedure.js";
2
+ import { deserializeListMetadata } from "../shared/serialize.js";
3
+ import { LIST_MENU_ITEM_ERROR_MAP } from "./errors.js";
4
+ import { ListMenuInput, MenuGroupItemList, MenuItemList } from "./schema.js";
5
+ import { buildMenuGroupItem, deserializeListMenuInput, extractSlugFromUrl } from "./utils.js";
6
+ import { stringifiedMetaRecord } from "@kizlo/shared";
7
+
8
+ //#region src/menu/index.ts
9
+ const MENU_ROUTER_MAP = { items: {
10
+ list: createProcedure({
11
+ scope: "api",
12
+ method: "GET",
13
+ path: "/site/menus",
14
+ output: MenuItemList,
15
+ query: ListMenuInput.optional(),
16
+ errors: LIST_MENU_ITEM_ERROR_MAP
17
+ }, async ({ context, errors, input }) => {
18
+ const response = await context.service.wordpress.menus.items.list(deserializeListMenuInput(input.query));
19
+ if (response.error) switch (response.error.code) {
20
+ case "rest_post_invalid_page_number": throw errors.MENU_ITEM_INVALID_PAGE();
21
+ case "rest_cannot_view": throw errors.MENU_ITEM_FORBIDDEN();
22
+ default:
23
+ context.logger.error("List menu items unhandled error", response.error, { code: response.error.code });
24
+ throw errors.INTERNAL_SERVER_ERROR();
25
+ }
26
+ return {
27
+ items: response.data.items.map((item) => {
28
+ const href = item.object !== "custom" ? extractSlugFromUrl(item.url) : item.url;
29
+ return {
30
+ id: item.id,
31
+ href,
32
+ name: item.title.rendered,
33
+ description: item.description,
34
+ type: item.object,
35
+ meta: stringifiedMetaRecord(item.meta ?? {})
36
+ };
37
+ }),
38
+ meta: deserializeListMetadata(response.data.meta)
39
+ };
40
+ }),
41
+ groupList: createProcedure({
42
+ scope: "api",
43
+ method: "GET",
44
+ path: "/menus/group",
45
+ query: ListMenuInput.optional(),
46
+ errors: LIST_MENU_ITEM_ERROR_MAP,
47
+ output: MenuGroupItemList
48
+ }, async ({ context, errors, input }) => {
49
+ const response = await context.service.wordpress.menus.items.list(deserializeListMenuInput(input.query));
50
+ if (response.error) switch (response.error.code) {
51
+ case "rest_post_invalid_page_number": throw errors.MENU_ITEM_INVALID_PAGE();
52
+ case "rest_cannot_view": throw errors.MENU_ITEM_FORBIDDEN();
53
+ default:
54
+ context.logger.error("Group list menu items unhandled error", response.error, { code: response.error.code });
55
+ throw errors.INTERNAL_SERVER_ERROR();
56
+ }
57
+ const itemMap = /* @__PURE__ */ new Map();
58
+ response.data.items.forEach((item) => {
59
+ itemMap.set(item.id, item);
60
+ });
61
+ const rootItems = response.data.items.filter((item) => item.parent === 0 || !itemMap.has(item.parent)).sort((a, b) => a.menu_order - b.menu_order);
62
+ return {
63
+ meta: deserializeListMetadata(response.data.meta),
64
+ items: rootItems.map((rootItem) => buildMenuGroupItem(rootItem, response.data.items))
65
+ };
66
+ })
67
+ } };
68
+
69
+ //#endregion
70
+ export { MENU_ROUTER_MAP };
@@ -0,0 +1,70 @@
1
+ import { ListMetadata, ListOrder } from "../shared/schema.js";
2
+ import { Metadata, NumberLike, arrayable } from "@kizlo/shared";
3
+ import z from "zod/v4";
4
+
5
+ //#region src/menu/schema.ts
6
+ const MENU_TYPES = [
7
+ "page",
8
+ "post",
9
+ "category",
10
+ "product",
11
+ "product_cat",
12
+ "product_tag",
13
+ "custom"
14
+ ];
15
+ const MenuType = z.enum(MENU_TYPES);
16
+ const MenuItem = z.object({
17
+ id: z.number(),
18
+ href: z.string(),
19
+ name: z.string(),
20
+ type: MenuType,
21
+ description: z.string(),
22
+ meta: Metadata
23
+ });
24
+ const MenuGroupItem = z.lazy(() => MenuItem.extend({
25
+ items: z.array(MenuGroupItem),
26
+ hasItems: z.boolean(),
27
+ meta: Metadata
28
+ }));
29
+ const MenuItemList = z.object({
30
+ items: z.array(MenuItem),
31
+ meta: ListMetadata
32
+ });
33
+ const MenuGroupItemList = z.object({
34
+ items: z.array(MenuGroupItem),
35
+ meta: ListMetadata
36
+ });
37
+ const MENU_ITEM_ORDER_BYES = [
38
+ "author",
39
+ "date",
40
+ "id",
41
+ "include",
42
+ "parent",
43
+ "relevance",
44
+ "slug",
45
+ "include_slugs",
46
+ "title",
47
+ "menu_order"
48
+ ];
49
+ const MenuItemOrderBy = z.enum(MENU_ITEM_ORDER_BYES);
50
+ const ListMenuInput = z.object({
51
+ page: NumberLike.optional(),
52
+ perPage: NumberLike.optional(),
53
+ search: z.string().optional(),
54
+ after: z.string().optional(),
55
+ before: z.string().optional(),
56
+ exclude: arrayable(NumberLike).optional(),
57
+ include: arrayable(NumberLike).optional(),
58
+ offset: NumberLike.optional(),
59
+ order: ListOrder.optional(),
60
+ orderby: MenuItemOrderBy.optional(),
61
+ searchColumns: z.array(z.string()).optional(),
62
+ slug: arrayable(z.string()).optional(),
63
+ taxRelation: z.enum(["AND", "OR"]).optional(),
64
+ menus: arrayable(NumberLike).optional(),
65
+ menusExclude: arrayable(NumberLike).optional(),
66
+ menuOrder: NumberLike.optional()
67
+ });
68
+
69
+ //#endregion
70
+ export { ListMenuInput, MenuGroupItemList, MenuItemList };
@@ -0,0 +1,57 @@
1
+ import { stringifiedMetaRecord } from "@kizlo/shared";
2
+
3
+ //#region src/menu/utils.ts
4
+ function deserializeListMenuInput(input) {
5
+ return {
6
+ context: "edit",
7
+ after: input?.after,
8
+ before: input?.before,
9
+ exclude: input?.exclude,
10
+ include: input?.include,
11
+ menu_order: input?.menuOrder,
12
+ menus: input?.menus,
13
+ menus_exclude: input?.menusExclude,
14
+ offset: input?.offset,
15
+ order: input?.order,
16
+ orderby: input?.orderby,
17
+ page: input?.page,
18
+ per_page: input?.perPage,
19
+ search: input?.search,
20
+ slug: input?.slug,
21
+ tax_relation: input?.taxRelation,
22
+ search_columns: input?.searchColumns,
23
+ status: "publish"
24
+ };
25
+ }
26
+ function extractSlugFromUrl(url) {
27
+ if (!url || url.trim() === "") return "/";
28
+ const trimmed = url.trim();
29
+ let pathname;
30
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) try {
31
+ pathname = new URL(trimmed).pathname;
32
+ } catch {
33
+ pathname = trimmed;
34
+ }
35
+ else pathname = trimmed;
36
+ const cleanPath = pathname.replace(/^\/+|\/+$/g, "");
37
+ if (!cleanPath) return "/";
38
+ const segments = cleanPath.split("/");
39
+ return segments[segments.length - 1] ?? "/";
40
+ }
41
+ function buildMenuGroupItem(wpItem, items) {
42
+ const children = items.filter((item) => item.parent === wpItem.id).sort((a, b) => a.menu_order - b.menu_order).map((child) => buildMenuGroupItem(child, items));
43
+ const href = wpItem.object !== "custom" ? extractSlugFromUrl(wpItem.url) : wpItem.url;
44
+ return {
45
+ id: wpItem.id,
46
+ href,
47
+ name: wpItem.title.rendered,
48
+ description: wpItem.description,
49
+ type: wpItem.object,
50
+ items: children,
51
+ hasItems: children.length > 0,
52
+ meta: stringifiedMetaRecord(wpItem.meta ?? {})
53
+ };
54
+ }
55
+
56
+ //#endregion
57
+ export { buildMenuGroupItem, deserializeListMenuInput, extractSlugFromUrl };
@@ -0,0 +1,2 @@
1
+ import { KizloServerOptions, createKizlo } from "../integrations/nextjs/kizlo.server.js";
2
+ export { KizloServerOptions, createKizlo };
@@ -0,0 +1,3 @@
1
+ import { createKizlo } from "../integrations/nextjs/kizlo.server.js";
2
+
3
+ export { createKizlo };
@@ -0,0 +1,2 @@
1
+ import { KizloClientOptions, createKizloClient } from "./integrations/nextjs/kizlo.client.js";
2
+ export { KizloClientOptions, createKizloClient };
package/dist/nextjs.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createKizloClient } from "./integrations/nextjs/kizlo.client.js";
2
+
3
+ export { createKizloClient };
@@ -0,0 +1,5 @@
1
+ //#region package.json
2
+ var version = "0.1.0";
3
+
4
+ //#endregion
5
+ export { version };
@@ -0,0 +1,30 @@
1
+ import { defineErrorMap } from "../shared/error.js";
2
+
3
+ //#region src/post/error.ts
4
+ const GET_POST_ERROR_MAP = defineErrorMap({
5
+ POST_NOT_FOUND: {
6
+ status: 404,
7
+ message: "Post not found"
8
+ },
9
+ POST_PASSWORD_INVALID: {
10
+ status: 400,
11
+ message: "Invalid post password."
12
+ }
13
+ });
14
+ const LIST_POST_ERROR_MAP = defineErrorMap({
15
+ POST_INVALID_PAGE: {
16
+ status: 400,
17
+ message: "The requested page does not exist."
18
+ },
19
+ POST_SEARCH_REQUIRED: {
20
+ status: 400,
21
+ message: "A search term is required."
22
+ },
23
+ POST_ORDERBY_INCLUDE_MISSING: {
24
+ status: 400,
25
+ message: "Ordering by 'include' requires the include parameter."
26
+ }
27
+ });
28
+
29
+ //#endregion
30
+ export { GET_POST_ERROR_MAP, LIST_POST_ERROR_MAP };
@@ -0,0 +1,8 @@
1
+ import { Fixture } from "../cli/wp/types.js";
2
+ import "../test.js";
3
+
4
+ //#region src/post/fixture.d.ts
5
+ /** Core post fixture: seeds sample posts + comments so the post/comment suites have content to read. */
6
+ declare function postFixture(): Fixture;
7
+ //#endregion
8
+ export { postFixture };
@@ -0,0 +1,71 @@
1
+ import { defineFixture } from "../cli/wp/types.js";
2
+
3
+ //#region src/post/fixture.ts
4
+ const POSTS = [{
5
+ slug: "hello-world-test",
6
+ title: "Hello World Test",
7
+ content: "First seeded post.",
8
+ comments: [{
9
+ author: "customer",
10
+ email: "customer@example.com",
11
+ content: "Seed comment"
12
+ }]
13
+ }, {
14
+ slug: "second-test-post",
15
+ title: "Second Test Post",
16
+ content: "Second seeded post."
17
+ }];
18
+ async function upsertPost(ctx, post) {
19
+ const existing = await ctx.service.posts.list({
20
+ slug: post.slug,
21
+ per_page: 1
22
+ });
23
+ if (existing.error) throw existing.error;
24
+ if (existing.data.items[0]) return existing.data.items[0].id;
25
+ const created = await ctx.service.posts.create({
26
+ slug: post.slug,
27
+ title: post.title,
28
+ content: post.content,
29
+ status: "publish",
30
+ author: ctx.adminId
31
+ });
32
+ if (created.error) throw created.error;
33
+ return created.data.id;
34
+ }
35
+ async function seedComments(ctx, postId, comments) {
36
+ if (!comments?.length) return;
37
+ const existing = await ctx.service.comments.list({
38
+ post: postId,
39
+ per_page: 1
40
+ });
41
+ if (existing.error) throw existing.error;
42
+ if (existing.data.items.length > 0) return;
43
+ for (const comment of comments) {
44
+ const created = await ctx.service.comments.create({
45
+ post: postId,
46
+ content: comment.content,
47
+ author_name: comment.author,
48
+ author_email: comment.email,
49
+ status: "approve"
50
+ });
51
+ if (created.error) throw created.error;
52
+ }
53
+ }
54
+ /** Core post fixture: seeds sample posts + comments so the post/comment suites have content to read. */
55
+ function postFixture() {
56
+ return defineFixture({
57
+ name: "post",
58
+ async seed(ctx) {
59
+ let postId = 0;
60
+ for (const post of POSTS) {
61
+ const id = await upsertPost(ctx, post);
62
+ if (!postId) postId = id;
63
+ await seedComments(ctx, id, post.comments);
64
+ }
65
+ return { postId };
66
+ }
67
+ });
68
+ }
69
+
70
+ //#endregion
71
+ export { postFixture };
@@ -0,0 +1,123 @@
1
+ import { createProcedure } from "../shared/procedure.js";
2
+ import { compare } from "../shared/crypto.js";
3
+ import { deserializeListMetadata } from "../shared/serialize.js";
4
+ import { getPostTypeService } from "../post-type/service.js";
5
+ import { parseIdentifier } from "../shared/identifier.js";
6
+ import { GET_POST_ERROR_MAP, LIST_POST_ERROR_MAP } from "./error.js";
7
+ import { ListPostInput, Post, PostList, RetrievePostInput } from "./schema.js";
8
+ import { deserializePost } from "./utils.js";
9
+
10
+ //#region src/post/index.ts
11
+ const POST_ROUTER_MAP = {
12
+ get: createProcedure({
13
+ scope: "api",
14
+ method: "GET",
15
+ path: "/posts/{identifier}",
16
+ params: RetrievePostInput.pick({ identifier: true }),
17
+ query: RetrievePostInput.pick({
18
+ password: true,
19
+ previewToken: true
20
+ }).optional(),
21
+ output: Post,
22
+ errors: GET_POST_ERROR_MAP
23
+ }, async ({ input, context, errors }) => {
24
+ const postType = getPostTypeService("post", context.service.wordpress);
25
+ if (input.query?.previewToken) {
26
+ const result = await context.verifyPreviewToken(input.query.previewToken);
27
+ if (!result) throw errors.POST_NOT_FOUND();
28
+ const response$1 = await postType.get({
29
+ type: "id",
30
+ value: result.id
31
+ });
32
+ if (response$1.error) switch (response$1.error.code) {
33
+ case "rest_post_invalid_id": throw errors.POST_NOT_FOUND();
34
+ case "rest_post_incorrect_password": throw errors.POST_PASSWORD_INVALID();
35
+ default:
36
+ context.logger.error("Get post preview unhandled error", response$1.error, {
37
+ id: result.id,
38
+ code: response$1.error.code
39
+ });
40
+ throw errors.INTERNAL_SERVER_ERROR();
41
+ }
42
+ return deserializePost({
43
+ ...response$1.data,
44
+ password: ""
45
+ });
46
+ }
47
+ const identifier = parseIdentifier(input.params.identifier);
48
+ if (!identifier) throw errors.POST_NOT_FOUND();
49
+ const response = await postType.get(identifier);
50
+ if (response.error) switch (response.error.code) {
51
+ case "rest_post_invalid_id": throw errors.POST_NOT_FOUND();
52
+ case "rest_post_incorrect_password": throw errors.POST_PASSWORD_INVALID();
53
+ default:
54
+ context.logger.error("Get post unhandled error", response.error, {
55
+ identifier,
56
+ code: response.error.code
57
+ });
58
+ throw errors.INTERNAL_SERVER_ERROR();
59
+ }
60
+ const data = response.data;
61
+ if (input.query?.password && data.password) {
62
+ if (!compare(input.query.password, data.password)) throw errors.POST_PASSWORD_INVALID();
63
+ return deserializePost({
64
+ ...data,
65
+ password: ""
66
+ });
67
+ }
68
+ return deserializePost(data);
69
+ }),
70
+ list: createProcedure({
71
+ scope: "api",
72
+ method: "GET",
73
+ path: "/posts",
74
+ query: ListPostInput.optional(),
75
+ output: PostList,
76
+ errors: LIST_POST_ERROR_MAP
77
+ }, async ({ input, context, errors }) => {
78
+ const response = await getPostTypeService("post", context.service.wordpress).list({
79
+ context: "edit",
80
+ status: "publish",
81
+ after: input.query?.after,
82
+ author: input.query?.author,
83
+ author_exclude: input.query?.authorExclude,
84
+ before: input.query?.before,
85
+ categories: input.query?.categories,
86
+ categories_exclude: input.query?.categoriesExclude,
87
+ exclude: input.query?.exclude,
88
+ include: input.query?.include,
89
+ offset: input.query?.offset,
90
+ order: input.query?.order,
91
+ orderby: input.query?.orderby,
92
+ page: input.query?.page,
93
+ per_page: input.query?.perPage,
94
+ search: input.query?.search,
95
+ search_columns: input.query?.searchColumns,
96
+ slug: input.query?.slug,
97
+ sticky: input.query?.sticky,
98
+ tags: input.query?.tags,
99
+ tags_exclude: input.query?.tagsExclude,
100
+ tax_relation: input.query?.taxRelation
101
+ });
102
+ if (response.error) switch (response.error.code) {
103
+ case "rest_post_invalid_page_number": throw errors.POST_INVALID_PAGE();
104
+ case "rest_no_search_term_defined": throw errors.POST_SEARCH_REQUIRED();
105
+ case "rest_orderby_include_missing_include": throw errors.POST_ORDERBY_INCLUDE_MISSING();
106
+ default:
107
+ context.logger.error("List posts unhandled error", response.error, { code: response.error.code });
108
+ throw errors.INTERNAL_SERVER_ERROR();
109
+ }
110
+ const list = context.service.wordpress.resolveList({
111
+ data: response.data,
112
+ headers: response.headers,
113
+ searchParams: input.query
114
+ });
115
+ return {
116
+ meta: deserializeListMetadata(list.meta),
117
+ items: list.items.map(deserializePost)
118
+ };
119
+ })
120
+ };
121
+
122
+ //#endregion
123
+ export { POST_ROUTER_MAP };