Compare commits
7 Commits
0ddb9da94a
...
92e80bd93a
| Author | SHA1 | Date | |
|---|---|---|---|
| 92e80bd93a | |||
| 2f2064a803 | |||
| cfb6a1cf11 | |||
| e8598e418d | |||
| ad9c26e278 | |||
| 9464a5c85f | |||
| 8ea5c56ba5 |
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||
//npm.cnb.cool/kevisual/registry/-/packages/:_authToken=${CNB_API_KEY}
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
13
.vscode/mcp.json
vendored
Normal file
13
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"servers": {
|
||||
"convex": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"convex@latest",
|
||||
"mcp",
|
||||
"start"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
52
convex/_generated/api.d.ts
vendored
52
convex/_generated/api.d.ts
vendored
@@ -8,8 +8,10 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as github_action from "../github/action.js";
|
||||
import type * as github_starrred from "../github/starrred.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as nCode from "../nCode.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
@@ -18,8 +20,10 @@ import type {
|
||||
} from "convex/server";
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
"github/action": typeof github_action;
|
||||
"github/starrred": typeof github_starrred;
|
||||
http: typeof http;
|
||||
nCode: typeof nCode;
|
||||
}>;
|
||||
|
||||
/**
|
||||
@@ -48,4 +52,50 @@ export declare const internal: FilterApi<
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
|
||||
export declare const components: {};
|
||||
export declare const components: {
|
||||
nCode: {
|
||||
action: {
|
||||
create: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
code: string;
|
||||
data: any;
|
||||
description: string;
|
||||
slug?: string;
|
||||
title: string;
|
||||
type: string;
|
||||
userId: string;
|
||||
},
|
||||
any
|
||||
>;
|
||||
deleteBySlug: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{ slug: string },
|
||||
any
|
||||
>;
|
||||
getByCode: FunctionReference<"query", "internal", { code: string }, any>;
|
||||
getBySlug: FunctionReference<"query", "internal", { slug: string }, any>;
|
||||
getList: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{ type?: string; userId?: string },
|
||||
any
|
||||
>;
|
||||
updateBySlug: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
code?: string;
|
||||
data?: any;
|
||||
description?: string;
|
||||
slug: string;
|
||||
title?: string;
|
||||
type?: string;
|
||||
},
|
||||
any
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { AuthConfig } from "convex/server";
|
||||
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
|
||||
50
convex/components/n-code/_generated/api.ts
Normal file
50
convex/components/n-code/_generated/api.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as action from "../action.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
import { anyApi, componentsGeneric } from "convex/server";
|
||||
|
||||
const fullApi: ApiFromModules<{
|
||||
action: typeof action;
|
||||
}> = anyApi as any;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's public API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
> = anyApi as any;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's internal API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = internal.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
> = anyApi as any;
|
||||
|
||||
export const components = componentsGeneric() as unknown as {};
|
||||
85
convex/components/n-code/_generated/component.ts
Normal file
85
convex/components/n-code/_generated/component.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `ComponentApi` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { FunctionReference } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing a Convex component's exposed API.
|
||||
*
|
||||
* Useful when expecting a parameter like `components.myComponent`.
|
||||
* Usage:
|
||||
* ```ts
|
||||
* async function myFunction(ctx: QueryCtx, component: ComponentApi) {
|
||||
* return ctx.runQuery(component.someFile.someQuery, { ...args });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||
{
|
||||
action: {
|
||||
create: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
code: string;
|
||||
data: any;
|
||||
description: string;
|
||||
slug?: string;
|
||||
title: string;
|
||||
type: string;
|
||||
userId: string;
|
||||
},
|
||||
any,
|
||||
Name
|
||||
>;
|
||||
deleteBySlug: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{ slug: string },
|
||||
any,
|
||||
Name
|
||||
>;
|
||||
getByCode: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{ code: string },
|
||||
any,
|
||||
Name
|
||||
>;
|
||||
getBySlug: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{ slug: string },
|
||||
any,
|
||||
Name
|
||||
>;
|
||||
getList: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{ type?: string; userId?: string },
|
||||
any,
|
||||
Name
|
||||
>;
|
||||
updateBySlug: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
code?: string;
|
||||
data?: any;
|
||||
description?: string;
|
||||
slug: string;
|
||||
title?: string;
|
||||
type?: string;
|
||||
},
|
||||
any,
|
||||
Name
|
||||
>;
|
||||
};
|
||||
};
|
||||
60
convex/components/n-code/_generated/dataModel.ts
Normal file
60
convex/components/n-code/_generated/dataModel.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated data model types.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
DataModelFromSchemaDefinition,
|
||||
DocumentByName,
|
||||
TableNamesInDataModel,
|
||||
SystemTableNames,
|
||||
} from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
import schema from "../schema.js";
|
||||
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||
DataModel,
|
||||
TableName
|
||||
>;
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
*
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||
GenericId<TableName>;
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
*
|
||||
* This type includes information about what tables you have, the type of
|
||||
* documents stored in those tables, and the indexes defined on them.
|
||||
*
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
||||
156
convex/components/n-code/_generated/server.ts
Normal file
156
convex/components/n-code/_generated/server.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
ActionBuilder,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const query: QueryBuilder<DataModel, "public"> = queryGeneric;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalQuery: QueryBuilder<DataModel, "internal"> =
|
||||
internalQueryGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const mutation: MutationBuilder<DataModel, "public"> = mutationGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalMutation: MutationBuilder<DataModel, "internal"> =
|
||||
internalMutationGeneric;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const action: ActionBuilder<DataModel, "public"> = actionGeneric;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalAction: ActionBuilder<DataModel, "internal"> =
|
||||
internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
*
|
||||
* The query context is passed as the first argument to any Convex query
|
||||
* function run on the server.
|
||||
*
|
||||
* If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
*
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*
|
||||
* If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
*
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
*
|
||||
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
* functions.
|
||||
*
|
||||
* Convex guarantees that all writes within a single mutation are
|
||||
* executed atomically, so you never have to worry about partial writes leaving
|
||||
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||
* for the guarantees Convex provides your functions.
|
||||
*/
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||
139
convex/components/n-code/action.ts
Normal file
139
convex/components/n-code/action.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { query, mutation } from "./_generated/server.ts";
|
||||
import { v } from "convex/values";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
const randomId = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 10);
|
||||
/**
|
||||
* 查询 nCode 列表
|
||||
*/
|
||||
export const getList = query({
|
||||
args: {
|
||||
userId: v.optional(v.string()),
|
||||
type: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// 优先使用索引过滤
|
||||
if (args.userId) {
|
||||
let results = await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_userId", (q) => q.eq("userId", args.userId!))
|
||||
.collect();
|
||||
if (args.type) {
|
||||
results = results.filter((item) => item.type === args.type);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
if (args.type) {
|
||||
return await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_type", (q) => q.eq("type", args.type!))
|
||||
.collect();
|
||||
}
|
||||
return await ctx.db.query("shortLink").collect();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据业务 id 查询单条记录
|
||||
*/
|
||||
export const getBySlug = query({
|
||||
args: { slug: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
|
||||
.first();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据 code 查询单条记录
|
||||
*/
|
||||
export const getByCode = query({
|
||||
args: {
|
||||
code: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const result = await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_code", (q) => q.eq("code", args.code))
|
||||
.first();
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建 nCode
|
||||
*/
|
||||
export const create = mutation({
|
||||
args: {
|
||||
slug: v.optional(v.string()),
|
||||
code: v.string(),
|
||||
type: v.string(),
|
||||
data: v.any(),
|
||||
title: v.string(),
|
||||
description: v.string(),
|
||||
userId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const slug = args.slug ?? randomId();
|
||||
// 唯一性检查
|
||||
const existing = await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_slug", (q) => q.eq("slug", slug))
|
||||
.first();
|
||||
if (existing) {
|
||||
throw new Error(`nCode with slug "${slug}" already exists`);
|
||||
}
|
||||
const result = await ctx.db.insert("shortLink", {
|
||||
version: "1.0", data: {
|
||||
...args.data,
|
||||
permission: {
|
||||
share: 'public',
|
||||
...(args.data?.permission || {})
|
||||
}
|
||||
}, ...args, slug
|
||||
});
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新 nCode
|
||||
*/
|
||||
export const updateBySlug = mutation({
|
||||
args: {
|
||||
slug: v.string(),
|
||||
code: v.optional(v.string()),
|
||||
type: v.optional(v.string()),
|
||||
data: v.optional(v.any()),
|
||||
title: v.optional(v.string()),
|
||||
description: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { slug, ...rest } = args;
|
||||
const record = await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_slug", (q) => q.eq("slug", slug))
|
||||
.first();
|
||||
if (!record) throw new Error(`nCode with slug "${slug}" not found`);
|
||||
await ctx.db.patch(record._id, rest);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除 nCode
|
||||
*/
|
||||
export const deleteBySlug = mutation({
|
||||
args: {
|
||||
slug: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const record = await ctx.db
|
||||
.query("shortLink")
|
||||
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
|
||||
.first();
|
||||
if (!record) throw new Error(`nCode with slug "${args.slug}" not found`);
|
||||
await ctx.db.delete(record._id);
|
||||
},
|
||||
});
|
||||
3
convex/components/n-code/convex.config.ts
Normal file
3
convex/components/n-code/convex.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineComponent } from "convex/server";
|
||||
const component = defineComponent("nCode");
|
||||
export default component;
|
||||
58
convex/components/n-code/schema.ts
Normal file
58
convex/components/n-code/schema.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export type Permission = {
|
||||
share?: 'public' | 'private' | 'protected';
|
||||
usernames?: string[]; // 仅当 type 为 'protected' 时有效
|
||||
password?: string; // 仅当 type 为 'protected' 时有效
|
||||
'expiration-time'?: string; // ISO 8601 格式的过期时间
|
||||
}
|
||||
// shortLink的使用方式
|
||||
// link模式,linkType默认为redirect,使用302跳转到目标地址
|
||||
// 1. 短链跳转,直接访问对用的地址
|
||||
// 2. 请求模式,
|
||||
// 1. 访问短链去请求另一个接口,内容是code: 200,判断是否执行成功(默认不透传结果)
|
||||
//
|
||||
export default defineSchema({
|
||||
// Other tables here...
|
||||
shortLink: defineTable({
|
||||
// 对外暴露的唯一业务 ID,nanoid 生成
|
||||
slug: v.string(),
|
||||
// 协作码,管理员才能编辑, 6-12 位随机字符串,唯一
|
||||
code: v.string(),
|
||||
// 码的类型,link, agent,默认值为 link
|
||||
type: v.string(),
|
||||
// 执行动作
|
||||
data: v.object({
|
||||
// action: v.string(), // 动作类型,如 "open_url", "run_agent" 等
|
||||
permission: v.optional(v.any()), // 权限设置
|
||||
// 仅当 type 为 'link' 时有效,表示跳转链接
|
||||
link: v.optional(v.string()),
|
||||
// 仅当 type 为 'link' 时有效,表示链接类型,如 'redirect' 等,默认为 'redirect'
|
||||
linkType: v.optional(v.string()),
|
||||
}),
|
||||
// 码的标题
|
||||
title: v.string(),
|
||||
// 描述定义
|
||||
description: v.string(),
|
||||
// 标签
|
||||
tags: v.optional(v.array(v.string())),
|
||||
// 绑定用户
|
||||
userId: v.string(),
|
||||
|
||||
version: v.optional(v.string()), // 版本号,默认为 "1.0"
|
||||
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
.index("by_userId", ["userId"])
|
||||
.index("by_code", ["code"])
|
||||
.index("by_type", ["type"]),
|
||||
desc: defineTable({
|
||||
slug: v.string(),
|
||||
// 卡片的正面图片 URL
|
||||
front: v.optional(v.string()),
|
||||
// 卡片的背面图片 URL
|
||||
back: v.optional(v.string()),
|
||||
status: v.optional(v.string()), // 生成状态:active, completed, failed, sold(卖掉了)
|
||||
}).index("by_slug", ["slug"]),
|
||||
});
|
||||
7
convex/convex.config.ts
Normal file
7
convex/convex.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineApp } from "convex/server";
|
||||
import NCode from './components/n-code/convex.config.ts';
|
||||
const app = defineApp();
|
||||
|
||||
app.use(NCode);
|
||||
|
||||
export default app;
|
||||
75
convex/github/action.ts
Normal file
75
convex/github/action.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { query, mutation, action } from "../_generated/server.js";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const getList = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const results = await ctx.db.query("github_starred").collect();
|
||||
return results;
|
||||
},
|
||||
});
|
||||
export const get = query({
|
||||
args: {
|
||||
id: v.id("github_starred"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
},
|
||||
});
|
||||
|
||||
export const createStarred = mutation({
|
||||
args: {
|
||||
repo_id: v.number(),
|
||||
title: v.string(),
|
||||
description: v.string(),
|
||||
author: v.string(),
|
||||
stars: v.number(),
|
||||
last_updated: v.string(),
|
||||
link: v.string(),
|
||||
auto: v.optional(v.string()),
|
||||
summary: v.optional(v.string()),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
},
|
||||
handler: async (ctx, values) => {
|
||||
const repo_id = values.repo_id;
|
||||
const existing = await ctx.db.query("github_starred").withIndex("by_repo_id", (q) =>
|
||||
q.eq("repo_id", repo_id)
|
||||
).first();
|
||||
if (existing) {
|
||||
// 更新已有记录
|
||||
const updated = await ctx.db.patch("github_starred", existing._id, values);
|
||||
return updated;
|
||||
}
|
||||
// 创建新记录
|
||||
const result = await ctx.db.insert("github_starred", values);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
export const updateById = mutation({
|
||||
args: {
|
||||
id: v.id("github_starred"),
|
||||
repo_id: v.number(),
|
||||
title: v.string(),
|
||||
description: v.string(),
|
||||
author: v.string(),
|
||||
stars: v.number(),
|
||||
last_updated: v.string(),
|
||||
link: v.string(),
|
||||
auto: v.optional(v.string()),
|
||||
summary: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...rest } = args;
|
||||
await ctx.db.patch(id, rest);
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteById = mutation({
|
||||
args: {
|
||||
id: v.id("github_starred"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.id);
|
||||
},
|
||||
});
|
||||
@@ -2,38 +2,17 @@ import { httpRouter } from "convex/server";
|
||||
import { httpAction } from "./_generated/server.js";
|
||||
const http = httpRouter();
|
||||
|
||||
http.route({
|
||||
path: "/auth",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
// 处理请求并返回响应
|
||||
return new Response(JSON.stringify({ message: "Hello from custom endpoint!" }), {
|
||||
const handler = httpAction(async (ctx, request) => {
|
||||
return new Response(JSON.stringify({ message: "Hello!" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
// 分开注册
|
||||
http.route({ path: "/api/router", method: "GET", handler });
|
||||
http.route({ path: "/api/router", method: "POST", handler });
|
||||
http.route({ path: "/api/router", method: "OPTIONS", handler });
|
||||
|
||||
|
||||
// https://api-convex.kevisual.cn/root/convex/jwks.json
|
||||
http.route({
|
||||
path: '/root/convex/jwks.json',
|
||||
method: 'GET',
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
// 返回 JWKS 数据
|
||||
const jwks = {
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"n": "km4cjJJOMFkl2G5qWMuFmWwF7rmeqRYzYdR8SddKeeMW0e9yIf5pv2Mfwv0aMJUpb-_j3j9M7whx_SEGc_2jx1vxCu1AlYURhnnLTWdsR-ZRPr2LK9UstMrgpWV425R2RliqXTDTYlSxUUlD9nPue_tqbfwN2aM9MCarm67xK_ZCcKRlW9o9L2-9UMfzRA7uiy4VQtOemP0PTXp-E9RxNiMPOQXIRls9wTW_EkDT3dGy7JCZhj7_qib3T8k9m84SwU7wI2R_3IH4DcHSMAn1BRRMXZ1_wPhbP39laNtdJgbDjGCqUccGhLUaoo2WGkZ52eb7NPqamp0K1Dh2jwTIJQ",
|
||||
"e": "AQAB",
|
||||
"kid": "kid-key-1"
|
||||
}
|
||||
]
|
||||
};
|
||||
return new Response(JSON.stringify(jwks), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}),
|
||||
})
|
||||
export default http;
|
||||
85
convex/nCode.ts
Normal file
85
convex/nCode.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 顶层包装文件 —— 将 nCode Component 的函数暴露为公共 API。
|
||||
*
|
||||
* Convex Component 内部函数对客户端是 internal 级别,
|
||||
* 必须在顶层 app 中用 ctx.runQuery / ctx.runMutation 做代理才能被客户端访问。
|
||||
*
|
||||
* 客户端调用示例:
|
||||
* import { api } from "../convex/_generated/api";
|
||||
* const list = useQuery(api.nCode.getList, { userId: "xxx" });
|
||||
*/
|
||||
import { query, mutation } from "./_generated/server.js";
|
||||
import { components } from "./_generated/api.js";
|
||||
import { v } from "convex/values";
|
||||
|
||||
/** 查询列表,支持按 userId / type 过滤 */
|
||||
export const getList = query({
|
||||
args: {
|
||||
userId: v.optional(v.string()),
|
||||
type: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.runQuery(components.nCode.action.getList, args);
|
||||
},
|
||||
});
|
||||
|
||||
/** 根据 code 查单条记录 */
|
||||
export const getByCode = query({
|
||||
args: {
|
||||
code: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.runQuery(components.nCode.action.getByCode, args);
|
||||
},
|
||||
});
|
||||
|
||||
/** 根据业务 id 查单条记录 */
|
||||
export const getBySlug = query({
|
||||
args: {
|
||||
slug: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.runQuery(components.nCode.action.getBySlug, args);
|
||||
},
|
||||
});
|
||||
|
||||
/** 创建 nCode */
|
||||
export const create = mutation({
|
||||
args: {
|
||||
slug: v.optional(v.string()),
|
||||
code: v.string(),
|
||||
type: v.string(),
|
||||
data: v.object({}),
|
||||
title: v.string(),
|
||||
description: v.string(),
|
||||
userId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.runMutation(components.nCode.action.create, args);
|
||||
},
|
||||
});
|
||||
|
||||
/** 更新 nCode */
|
||||
export const updateBySlug = mutation({
|
||||
args: {
|
||||
slug: v.string(),
|
||||
code: v.optional(v.string()),
|
||||
type: v.optional(v.string()),
|
||||
data: v.optional(v.object({})),
|
||||
title: v.optional(v.string()),
|
||||
description: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.runMutation(components.nCode.action.updateBySlug, args);
|
||||
},
|
||||
});
|
||||
|
||||
/** 删除 nCode */
|
||||
export const deleteBySlug = mutation({
|
||||
args: {
|
||||
slug: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.runMutation(components.nCode.action.deleteBySlug, args);
|
||||
},
|
||||
});
|
||||
@@ -4,16 +4,17 @@ import { v } from "convex/values";
|
||||
export default defineSchema({
|
||||
// Other tables here...
|
||||
github_starred: defineTable({
|
||||
author: v.string(),
|
||||
auto: v.string(),
|
||||
repo_id: v.number(),
|
||||
title: v.string(),
|
||||
description: v.string(),
|
||||
author: v.string(),
|
||||
stars: v.number(),
|
||||
last_updated: v.string(),
|
||||
link: v.string(),
|
||||
repo_id: v.float64(),
|
||||
stars: v.float64(),
|
||||
summary: v.string(),
|
||||
title: v.string(),
|
||||
}),
|
||||
auto: v.optional(v.string()),
|
||||
summary: v.optional(v.string()),
|
||||
}).index("by_repo_id", ["repo_id"])
|
||||
.index("by_title", ["title"]),
|
||||
users: defineTable({
|
||||
userId: v.string(), // 外部系统的用户 ID
|
||||
name: v.string(),
|
||||
@@ -26,4 +27,5 @@ export default defineSchema({
|
||||
expiresAt: v.optional(v.string()),
|
||||
token: v.optional(v.string()),
|
||||
}).index("token", ["token"]),
|
||||
|
||||
});
|
||||
20
package.json
20
package.json
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@kevisual/convex",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "bunx convex dev"
|
||||
"dev": "bunx convex dev",
|
||||
"mcp": "npx -y convex@latest mcp start --project-dir ./convex"
|
||||
},
|
||||
"files": [
|
||||
"convex",
|
||||
@@ -13,17 +14,22 @@
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^25.2.0"
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/node": "^25.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kevisual/auth": "^2.0.3",
|
||||
"convex": "1.31.7",
|
||||
"jose": "^6.1.3"
|
||||
"convex": "1.32.0",
|
||||
"jose": "^6.1.3",
|
||||
"nanoid": "^5.1.6"
|
||||
},
|
||||
"exports": {
|
||||
".": "./convex/_generated/api.js",
|
||||
"./admin.ts": "./convex/admin.ts"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@@ -12,21 +12,24 @@ importers:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
convex:
|
||||
specifier: 1.31.7
|
||||
version: 1.31.7
|
||||
specifier: 1.32.0
|
||||
version: 1.32.0
|
||||
jose:
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
devDependencies:
|
||||
'@kevisual/types':
|
||||
specifier: ^0.0.12
|
||||
version: 0.0.12
|
||||
'@types/bun':
|
||||
specifier: ^1.3.8
|
||||
version: 1.3.8
|
||||
specifier: ^1.3.9
|
||||
version: 1.3.9
|
||||
'@types/node':
|
||||
specifier: ^25.2.0
|
||||
version: 25.2.0
|
||||
specifier: ^25.3.3
|
||||
version: 25.3.3
|
||||
|
||||
packages:
|
||||
|
||||
@@ -192,17 +195,17 @@ packages:
|
||||
'@kevisual/types@0.0.12':
|
||||
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
|
||||
|
||||
'@types/bun@1.3.8':
|
||||
resolution: {integrity: sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA==}
|
||||
'@types/bun@1.3.9':
|
||||
resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==}
|
||||
|
||||
'@types/node@25.2.0':
|
||||
resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==}
|
||||
'@types/node@25.3.3':
|
||||
resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
|
||||
|
||||
bun-types@1.3.8:
|
||||
resolution: {integrity: sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q==}
|
||||
bun-types@1.3.9:
|
||||
resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==}
|
||||
|
||||
convex@1.31.7:
|
||||
resolution: {integrity: sha512-PtNMe1mAIOvA8Yz100QTOaIdgt2rIuWqencVXrb4McdhxBHZ8IJ1eXTnrgCC9HydyilGT1pOn+KNqT14mqn9fQ==}
|
||||
convex@1.32.0:
|
||||
resolution: {integrity: sha512-5FlajdLpW75pdLS+/CgGH5H6yeRuA+ru50AKJEYbJpmyILUS+7fdTvsdTaQ7ZFXMv0gE8mX4S+S3AtJ94k0mfw==}
|
||||
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -225,13 +228,30 @@ packages:
|
||||
jose@6.1.3:
|
||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||
|
||||
nanoid@5.1.6:
|
||||
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
prettier@3.8.1:
|
||||
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
ws@8.18.0:
|
||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
@@ -317,22 +337,26 @@ snapshots:
|
||||
|
||||
'@kevisual/types@0.0.12': {}
|
||||
|
||||
'@types/bun@1.3.8':
|
||||
'@types/bun@1.3.9':
|
||||
dependencies:
|
||||
bun-types: 1.3.8
|
||||
bun-types: 1.3.9
|
||||
|
||||
'@types/node@25.2.0':
|
||||
'@types/node@25.3.3':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
undici-types: 7.18.2
|
||||
|
||||
bun-types@1.3.8:
|
||||
bun-types@1.3.9:
|
||||
dependencies:
|
||||
'@types/node': 25.2.0
|
||||
'@types/node': 25.3.3
|
||||
|
||||
convex@1.31.7:
|
||||
convex@1.32.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.0
|
||||
prettier: 3.8.1
|
||||
ws: 8.18.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
esbuild@0.27.0:
|
||||
optionalDependencies:
|
||||
@@ -365,6 +389,10 @@ snapshots:
|
||||
|
||||
jose@6.1.3: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
|
||||
prettier@3.8.1: {}
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
9
readme.md
Normal file
9
readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 基于convex做一些小玩具
|
||||
|
||||
## 安装
|
||||
|
||||
```json
|
||||
{
|
||||
"@kevisual/convex":"git+https://cnb.cool/kevisual/convex"
|
||||
}
|
||||
```
|
||||
15
src/admin.ts
Normal file
15
src/admin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { api } from '../convex/_generated/api.js';
|
||||
import { ConvexClient, AuthTokenFetcher } from "convex/browser";
|
||||
const url = process.env["CONVEX_URL"]
|
||||
const client = new ConvexClient(url!);
|
||||
|
||||
const token = process.env["KEVISUAL_CONVEX_TOKEN"]
|
||||
const authTokenFetcher: AuthTokenFetcher = async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
|
||||
console.log("AuthTokenFetcher called, forceRefreshToken:", forceRefreshToken);
|
||||
return token;
|
||||
}
|
||||
client.setAuth(authTokenFetcher, (isAuthenticated) => {
|
||||
console.log("Auth isAuthenticated:", isAuthenticated);
|
||||
});
|
||||
|
||||
export { client, api }
|
||||
19
test/get-list.ts
Normal file
19
test/get-list.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { client, api } from '../src/admin.ts'
|
||||
|
||||
|
||||
// client.query(api.nCode.getList, {}).then((res) => {
|
||||
// console.log('getList', res)
|
||||
// })
|
||||
|
||||
const createNCode = async () => {
|
||||
const res = await client.mutation(api.nCode.create, {
|
||||
code: Math.random().toString(36).substring(2, 8),
|
||||
type: "link",
|
||||
data: {},
|
||||
title: "测试码",
|
||||
description: "这是一个测试码",
|
||||
userId: "test-user-id",
|
||||
})
|
||||
console.log('createNCode', res)
|
||||
}
|
||||
createNCode()
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"test/**/*",
|
||||
"convex/**/*",
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user