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
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 };
|
package/dist/nextjs.d.ts
ADDED
package/dist/nextjs.js
ADDED
package/dist/package.js
ADDED
|
@@ -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 };
|