mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-06 04:20:58 +00:00
wip
This commit is contained in:
23
.github/workflows/node.js.yml
vendored
Normal file
23
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Run tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
cache-dependency-path: |
|
||||||
|
example/package.json
|
||||||
|
package.json
|
||||||
|
node-version: "18.x"
|
||||||
|
cache: "npm"
|
||||||
|
- run: npm i
|
||||||
|
- run: npm ci
|
||||||
|
- run: cd example && npm i && cd ..
|
||||||
|
- run: npm test
|
||||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
*.local
|
||||||
|
*.log
|
||||||
|
/.vscode/
|
||||||
|
/docs/.vitepress/cache
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
explorations
|
||||||
|
node_modules
|
||||||
|
.eslintcache
|
||||||
|
# components are libraries!
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# this is a package-json-redirect stub dir, see https://github.com/andrewbranch/example-subpath-exports-ts-compat?tab=readme-ov-file
|
||||||
|
react/package.json
|
||||||
|
# npm pack output
|
||||||
|
*.tgz
|
||||||
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
50
CONTRIBUTING.md
Normal file
50
CONTRIBUTING.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Developing guide
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i
|
||||||
|
cd example
|
||||||
|
npm i
|
||||||
|
npx convex dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -rf dist/ && npm run build
|
||||||
|
npm run typecheck
|
||||||
|
npm run test
|
||||||
|
cd example
|
||||||
|
npm run lint
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
### Building a one-off package
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -rf dist/ && npm run build
|
||||||
|
npm pack
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploying a new version
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# this will change the version and commit it (if you run it in the root directory)
|
||||||
|
npm version patch
|
||||||
|
npm publish --dry-run
|
||||||
|
# sanity check files being included
|
||||||
|
npm publish
|
||||||
|
git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Alpha release
|
||||||
|
|
||||||
|
The same as above, but it requires extra flags so the release is only installed with `@alpha`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm version prerelease --preid alpha
|
||||||
|
npm publish --tag alpha
|
||||||
|
```
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
178
README.md
Normal file
178
README.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Convex Component Template
|
||||||
|
|
||||||
|
This is a Convex component, ready to be published on npm.
|
||||||
|
|
||||||
|
To create your own component:
|
||||||
|
|
||||||
|
1. Find and replace "Counter" to your component's Name.
|
||||||
|
1. Find and replace "counter" to your component's name.
|
||||||
|
1. Write code in src/component for your component.
|
||||||
|
1. Write code in src/client for your thick client.
|
||||||
|
1. Write example usage in example/convex/example.ts.
|
||||||
|
1. Delete the text in this readme until `---` and flesh out the README.
|
||||||
|
|
||||||
|
It is safe to find & replace "counter" project-wide.
|
||||||
|
|
||||||
|
To develop your component run a dev process in the example project.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm i
|
||||||
|
cd example
|
||||||
|
npm i
|
||||||
|
npx convex dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Modify the schema and index files in src/component/ to define your component.
|
||||||
|
|
||||||
|
Write a client for using this component in src/client/index.ts.
|
||||||
|
|
||||||
|
If you won't be adding frontend code (e.g. React components) to this
|
||||||
|
component you can delete the following:
|
||||||
|
|
||||||
|
- "prepack" and "postpack" scripts of package.json
|
||||||
|
- "./react" exports in package.json
|
||||||
|
- the "src/react/" directory
|
||||||
|
- the "node10stubs.mjs" file
|
||||||
|
|
||||||
|
### Component Directory structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── README.md documentation of your component
|
||||||
|
├── package.json component name, version number, other metadata
|
||||||
|
├── package-lock.json Components are like libraries, package-lock.json
|
||||||
|
│ is .gitignored and ignored by consumers.
|
||||||
|
├── src
|
||||||
|
│ ├── component/
|
||||||
|
│ │ ├── _generated/ Files here are generated.
|
||||||
|
│ │ ├── convex.config.ts Name your component here and use other components
|
||||||
|
│ │ ├── index.ts Define functions here and in new files in this directory
|
||||||
|
│ │ └── schema.ts schema specific to this component
|
||||||
|
│ ├── client/index.ts "Thick" client code goes here.
|
||||||
|
│ └── react/ Code intended to be used on the frontend goes here.
|
||||||
|
│ │ Your are free to delete this if this component
|
||||||
|
│ │ does not provide code.
|
||||||
|
│ └── index.ts
|
||||||
|
├── example/ example Convex app that uses this component
|
||||||
|
│ │ Run 'npx convex dev' from here during development.
|
||||||
|
│ ├── package.json.ts Thick client code goes here.
|
||||||
|
│ └── convex/
|
||||||
|
│ ├── _generated/
|
||||||
|
│ ├── convex.config.ts Imports and uses this component
|
||||||
|
│ ├── myFunctions.ts Functions that use the component
|
||||||
|
│ ├── schema.ts Example app schema
|
||||||
|
│ └── tsconfig.json
|
||||||
|
│
|
||||||
|
├── dist/ Publishing artifacts will be created here.
|
||||||
|
├── commonjs.json Used during build by TypeScript.
|
||||||
|
├── esm.json Used during build by TypeScript.
|
||||||
|
├── node10stubs.mjs Script used during build for compatibility
|
||||||
|
│ with the Metro bundler used with React Native.
|
||||||
|
├── eslint.config.mjs Recommended lints for writing a component.
|
||||||
|
│ Feel free to customize it.
|
||||||
|
└── tsconfig.json Recommended tsconfig.json for writing a component.
|
||||||
|
Some settings can be customized, some are required.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structure of a Convex Component
|
||||||
|
|
||||||
|
A Convex components exposes the entry point convex.config.js. The on-disk
|
||||||
|
location of this file must be a directory containing implementation files. These
|
||||||
|
files should be compiled to ESM.
|
||||||
|
The package.json should contain `"type": "module"` and the tsconfig.json should
|
||||||
|
contain `"moduleResolution": "Bundler"` or `"Node16"` in order to import other
|
||||||
|
component definitions.
|
||||||
|
|
||||||
|
In addition to convex.config.js, a component typically exposes a client that
|
||||||
|
wraps communication with the component for use in the Convex
|
||||||
|
environment is typically exposed as a named export `MyComponentClient` or
|
||||||
|
`MyComponent` imported from the root package.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { MyComponentClient } from "my-convex-component";
|
||||||
|
```
|
||||||
|
|
||||||
|
When frontend code is included it is typically published at a subpath:
|
||||||
|
|
||||||
|
```
|
||||||
|
import { helper } from "my-convex-component/react";
|
||||||
|
import { FrontendReactComponent } from "my-convex-component/react";
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend code should be compiled as CommonJS code as well as ESM and make use of
|
||||||
|
subpackage stubs (see next section).
|
||||||
|
|
||||||
|
If you do include frontend components, prefer peer dependencies to avoid using
|
||||||
|
more than one version of e.g. React.
|
||||||
|
|
||||||
|
### Support for Node10 module resolution
|
||||||
|
|
||||||
|
The [Metro](https://reactnative.dev/docs/metro) bundler for React Native
|
||||||
|
requires setting
|
||||||
|
[`resolver.unstable_enablePackageExports`](https://metrobundler.dev/docs/package-exports/)
|
||||||
|
in order to import code that lives in `dist/esm/react.js` from a path like
|
||||||
|
`my-convex-component/react`.
|
||||||
|
|
||||||
|
Authors of Convex component that provide frontend components are encouraged to
|
||||||
|
support these legacy "Node10-style" module resolution algorithms by generating
|
||||||
|
stub directories with special pre- and post-pack scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Convex Counter Component
|
||||||
|
|
||||||
|
[](https://badge.fury.io/js/@convex-dev%2Fcounter)
|
||||||
|
|
||||||
|
**Note: Convex Components are currently in beta**
|
||||||
|
|
||||||
|
<!-- START: Include on https://convex.dev/components -->
|
||||||
|
|
||||||
|
- [ ] What is some compelling syntax as a hook?
|
||||||
|
- [ ] Why should you use this component?
|
||||||
|
- [ ] Links to Stack / other resources?
|
||||||
|
|
||||||
|
Found a bug? Feature request? [File it here](https://github.com/get-convex/counter/issues).
|
||||||
|
|
||||||
|
## Pre-requisite: Convex
|
||||||
|
|
||||||
|
You'll need an existing Convex project to use the component.
|
||||||
|
Convex is a hosted backend platform, including a database, serverless functions,
|
||||||
|
and a ton more you can learn about [here](https://docs.convex.dev/get-started).
|
||||||
|
|
||||||
|
Run `npm create convex` or follow any of the [quickstarts](https://docs.convex.dev/home) to set one up.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the component package:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
npm install @convex-dev/counter
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a `convex.config.ts` file in your app's `convex/` folder and install the component by calling `use`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// convex/convex.config.ts
|
||||||
|
import { defineApp } from "convex/server";
|
||||||
|
import counter from "@convex-dev/counter/convex.config";
|
||||||
|
|
||||||
|
const app = defineApp();
|
||||||
|
app.use(counter);
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { components } from "./_generated/api";
|
||||||
|
import { Counter } from "@convex-dev/counter";
|
||||||
|
|
||||||
|
const counter = new Counter(components.counter, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See more example usage in [example.ts](./example/convex/example.ts).
|
||||||
|
|
||||||
|
<!-- END: Include on https://convex.dev/components -->
|
||||||
8
commonjs.json
Normal file
8
commonjs.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["src/**/*.test.*", "../src/package.json"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/commonjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
eslint.config.js
Normal file
44
eslint.config.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ files: ["src/**/*.{js,mjs,cjs,ts,tsx}"] },
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"dist/**",
|
||||||
|
"eslint.config.js",
|
||||||
|
"**/_generated/",
|
||||||
|
"node10stubs.mjs",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.worker,
|
||||||
|
parser: tseslint.parser,
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: ".",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"eslint-comments/no-unused-disable": "off",
|
||||||
|
|
||||||
|
// allow (_arg: number) => {} and const _foo = 1;
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
8
esm.json
Normal file
8
esm.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["src/**/*.test.*", "../src/package.json"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/esm"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
example/.gitignore
vendored
Normal file
16
example/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
!**/glob-import/dir/node_modules
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
*.cpuprofile
|
||||||
|
*.local
|
||||||
|
*.log
|
||||||
|
/.vscode/
|
||||||
|
/docs/.vitepress/cache
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
explorations
|
||||||
|
node_modules
|
||||||
|
playground-temp
|
||||||
|
temp
|
||||||
|
TODOs.md
|
||||||
|
.eslintcache
|
||||||
5
example/README.md
Normal file
5
example/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Example app
|
||||||
|
|
||||||
|
Components need an app that uses them in order to run codegen. An example app is also useful
|
||||||
|
for testing and documentation.
|
||||||
|
|
||||||
90
example/convex/README.md
Normal file
90
example/convex/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Welcome to your Convex functions directory!
|
||||||
|
|
||||||
|
Write your Convex functions here.
|
||||||
|
See https://docs.convex.dev/functions for more.
|
||||||
|
|
||||||
|
A query function that takes two arguments looks like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// functions.js
|
||||||
|
import { query } from "./_generated/server";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
export const myQueryFunction = query({
|
||||||
|
// Validators for arguments.
|
||||||
|
args: {
|
||||||
|
first: v.number(),
|
||||||
|
second: v.string(),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function implementation.
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
// Read the database as many times as you need here.
|
||||||
|
// See https://docs.convex.dev/database/reading-data.
|
||||||
|
const documents = await ctx.db.query("tablename").collect();
|
||||||
|
|
||||||
|
// Arguments passed from the client are properties of the args object.
|
||||||
|
console.log(args.first, args.second);
|
||||||
|
|
||||||
|
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||||
|
// remove non-public properties, or create new objects.
|
||||||
|
return documents;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Using this query function in a React component looks like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const data = useQuery(api.functions.myQueryFunction, {
|
||||||
|
first: 10,
|
||||||
|
second: "hello",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
A mutation function looks like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// functions.js
|
||||||
|
import { mutation } from "./_generated/server";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
export const myMutationFunction = mutation({
|
||||||
|
// Validators for arguments.
|
||||||
|
args: {
|
||||||
|
first: v.string(),
|
||||||
|
second: v.string(),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function implementation.
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
// Insert or modify documents in the database here.
|
||||||
|
// Mutations can also read from the database like queries.
|
||||||
|
// See https://docs.convex.dev/database/writing-data.
|
||||||
|
const message = { body: args.first, author: args.second };
|
||||||
|
const id = await ctx.db.insert("messages", message);
|
||||||
|
|
||||||
|
// Optionally, return a value from your mutation.
|
||||||
|
return await ctx.db.get(id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Using this mutation function in a React component looks like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const mutation = useMutation(api.functions.myMutationFunction);
|
||||||
|
function handleButtonPress() {
|
||||||
|
// fire and forget, the most common way to use mutations
|
||||||
|
mutation({ first: "Hello!", second: "me" });
|
||||||
|
// OR
|
||||||
|
// use the result once the mutation has completed
|
||||||
|
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||||
|
console.log(result),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the Convex CLI to push your functions to a deployment. See everything
|
||||||
|
the Convex CLI can do by running `npx convex -h` in your project root
|
||||||
|
directory. To learn more, launch the docs with `npx convex docs`.
|
||||||
63
example/convex/_generated/api.d.ts
vendored
Normal file
63
example/convex/_generated/api.d.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as example from "../example.js";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiFromModules,
|
||||||
|
FilterApi,
|
||||||
|
FunctionReference,
|
||||||
|
} from "convex/server";
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
declare const fullApi: ApiFromModules<{
|
||||||
|
example: typeof example;
|
||||||
|
}>;
|
||||||
|
declare const fullApiWithMounts: typeof fullApi;
|
||||||
|
|
||||||
|
export declare const api: FilterApi<
|
||||||
|
typeof fullApiWithMounts,
|
||||||
|
FunctionReference<any, "public">
|
||||||
|
>;
|
||||||
|
export declare const internal: FilterApi<
|
||||||
|
typeof fullApiWithMounts,
|
||||||
|
FunctionReference<any, "internal">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export declare const components: {
|
||||||
|
polar: {
|
||||||
|
lib: {
|
||||||
|
getOnboardingCheckoutUrl: FunctionReference<
|
||||||
|
"action",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
polarAccessToken: string;
|
||||||
|
successUrl: string;
|
||||||
|
userEmail: string;
|
||||||
|
userId: string;
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
listPlans: FunctionReference<"query", "internal", {}, any>;
|
||||||
|
setSubscriptionPending: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
any,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
23
example/convex/_generated/api.js
Normal file
23
example/convex/_generated/api.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { anyApi, componentsGeneric } from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const api = anyApi;
|
||||||
|
export const internal = anyApi;
|
||||||
|
export const components = componentsGeneric();
|
||||||
60
example/convex/_generated/dataModel.d.ts
vendored
Normal file
60
example/convex/_generated/dataModel.d.ts
vendored
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(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>;
|
||||||
149
example/convex/_generated/server.d.ts
vendored
Normal file
149
example/convex/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/* 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 {
|
||||||
|
ActionBuilder,
|
||||||
|
AnyComponents,
|
||||||
|
HttpActionBuilder,
|
||||||
|
MutationBuilder,
|
||||||
|
QueryBuilder,
|
||||||
|
GenericActionCtx,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
GenericDatabaseReader,
|
||||||
|
GenericDatabaseWriter,
|
||||||
|
FunctionReference,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
type GenericCtx =
|
||||||
|
| GenericActionCtx<DataModel>
|
||||||
|
| GenericMutationCtx<DataModel>
|
||||||
|
| GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const query: QueryBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const mutation: MutationBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const action: ActionBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* This 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 action in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export declare const httpAction: HttpActionBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* This differs from the {@link MutationCtx} because all of the services are
|
||||||
|
* read-only.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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>;
|
||||||
90
example/convex/_generated/server.js
Normal file
90
example/convex/_generated/server.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/* 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 {
|
||||||
|
actionGeneric,
|
||||||
|
httpActionGeneric,
|
||||||
|
queryGeneric,
|
||||||
|
mutationGeneric,
|
||||||
|
internalActionGeneric,
|
||||||
|
internalMutationGeneric,
|
||||||
|
internalQueryGeneric,
|
||||||
|
componentsGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = 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 = 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 = 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 = 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 = 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 = internalActionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a Convex HTTP action.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||||
|
* as its second.
|
||||||
|
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||||
|
*/
|
||||||
|
export const httpAction = httpActionGeneric;
|
||||||
7
example/convex/convex.config.ts
Normal file
7
example/convex/convex.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineApp } from "convex/server";
|
||||||
|
import polar from "@convex-dev/polar/convex.config";
|
||||||
|
|
||||||
|
const app = defineApp();
|
||||||
|
app.use(polar);
|
||||||
|
|
||||||
|
export default app;
|
||||||
60
example/convex/example.ts
Normal file
60
example/convex/example.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { internalMutation, query, mutation } from "./_generated/server";
|
||||||
|
import { components } from "./_generated/api";
|
||||||
|
import { Polar } from "@convex-dev/polar";
|
||||||
|
|
||||||
|
const polar = new Polar(components.polar);
|
||||||
|
|
||||||
|
/*
|
||||||
|
export const addOne = mutation({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
await numUsers.inc(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getCount = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
return await numUsers.count(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usingClient = internalMutation({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
await polar.add(ctx, "accomplishments");
|
||||||
|
await polar.add(ctx, "beans", 2);
|
||||||
|
const count = await polar.count(ctx, "beans");
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usingFunctions = internalMutation({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
await numUsers.inc(ctx);
|
||||||
|
await numUsers.inc(ctx);
|
||||||
|
await numUsers.dec(ctx);
|
||||||
|
return numUsers.count(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const directCall = internalMutation({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
await ctx.runMutation(components.polar.lib.add, {
|
||||||
|
name: "pennies",
|
||||||
|
count: 250,
|
||||||
|
});
|
||||||
|
await ctx.runMutation(components.polar.lib.add, {
|
||||||
|
name: "beans",
|
||||||
|
count: 3,
|
||||||
|
shards: 100,
|
||||||
|
});
|
||||||
|
const count = await ctx.runQuery(components.polar.lib.count, {
|
||||||
|
name: "beans",
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
*/
|
||||||
7
example/convex/schema.ts
Normal file
7
example/convex/schema.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineSchema } from "convex/server";
|
||||||
|
|
||||||
|
export default defineSchema(
|
||||||
|
{
|
||||||
|
// Any tables used by the example app go here.
|
||||||
|
},
|
||||||
|
);
|
||||||
31
example/convex/tsconfig.json
Normal file
31
example/convex/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
/* This TypeScript project config describes the environment that
|
||||||
|
* Convex functions run in and is used to typecheck them.
|
||||||
|
* You can modify it, but some settings required to use Convex.
|
||||||
|
*/
|
||||||
|
"compilerOptions": {
|
||||||
|
/* These settings are not required by Convex and can be modified. */
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* These compiler options are required by Convex */
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ES2021", "dom", "ESNext.Array", "DOM.Iterable"],
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* This should only be used in this example. Real apps should not attempt
|
||||||
|
* to compile TypeScript because differences between tsconfig.json files can
|
||||||
|
* cause the code to be compiled differently.
|
||||||
|
*/
|
||||||
|
"customConditions": ["@convex-dev/component-source"]
|
||||||
|
},
|
||||||
|
"include": ["./**/*"],
|
||||||
|
"exclude": ["./_generated"]
|
||||||
|
}
|
||||||
40
example/eslint.config.js
Normal file
40
example/eslint.config.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
ignores: ["convex"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
// Allow explicit `any`s
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
13
example/index.html
Normal file
13
example/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
example/package.json
Normal file
32
example/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "uses-component",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "convex dev --live-component-sources --typecheck-components",
|
||||||
|
"dev:frontend": "vite",
|
||||||
|
"logs": "convex logs",
|
||||||
|
"lint": "tsc -p convex && eslint convex"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@convex-dev/polar": "file:..",
|
||||||
|
"convex": "file:../node_modules/convex",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
"@eslint/js": "^9.9.0",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"eslint": "^9.9.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.9",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"typescript-eslint": "^8.0.1",
|
||||||
|
"vite": "^5.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
example/src/App.css
Normal file
42
example/src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
23
example/src/App.tsx
Normal file
23
example/src/App.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import "./App.css";
|
||||||
|
import { useMutation, useQuery } from "convex/react";
|
||||||
|
import { api } from "../convex/_generated/api";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const count = useQuery(api.example.getCount);
|
||||||
|
const addOne = useMutation(api.example.addOne);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Convex Polar Component Example</h1>
|
||||||
|
<div className="card">
|
||||||
|
<button onClick={() => addOne()}>count is {count}</button>
|
||||||
|
<p>
|
||||||
|
See <code>example/convex/example.ts</code> for all the ways to use
|
||||||
|
this component
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
68
example/src/index.css
Normal file
68
example/src/index.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
example/src/main.tsx
Normal file
17
example/src/main.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
const address = import.meta.env.VITE_CONVEX_URL;
|
||||||
|
|
||||||
|
const convex = new ConvexReactClient(address);
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ConvexProvider client={convex}>
|
||||||
|
<App />
|
||||||
|
</ConvexProvider>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
1
example/src/vite-env.d.ts
vendored
Normal file
1
example/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
24
example/tsconfig.json
Normal file
24
example/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"skipLibCheck": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* This should only be used in this example. Real apps should not attempt
|
||||||
|
* to compile TypeScript because differences between tsconfig.json files can
|
||||||
|
* cause the code to be compiled differently.
|
||||||
|
*/
|
||||||
|
"customConditions": ["@convex-dev/component-source"]
|
||||||
|
},
|
||||||
|
"include": ["./src", "vite.config.ts"]
|
||||||
|
}
|
||||||
10
example/vite.config.ts
Normal file
10
example/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
conditions: ["@convex-dev/component-source"],
|
||||||
|
},
|
||||||
|
});
|
||||||
86
node10stubs.mjs
Normal file
86
node10stubs.mjs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
async function findPackageJson(directory) {
|
||||||
|
const packagePath = path.join(directory, "package.json");
|
||||||
|
try {
|
||||||
|
await fs.access(packagePath);
|
||||||
|
return packagePath;
|
||||||
|
} catch (error) {
|
||||||
|
const parentDir = path.dirname(directory);
|
||||||
|
if (parentDir === directory) {
|
||||||
|
throw new Error("package.json not found");
|
||||||
|
}
|
||||||
|
return findPackageJson(parentDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processSubPackages(packageJsonPath, exports, cleanup = false) {
|
||||||
|
const baseDir = path.dirname(packageJsonPath);
|
||||||
|
|
||||||
|
for (const [subDir, _] of Object.entries(exports)) {
|
||||||
|
// package.json is already right where Node10 resolution would expect it.
|
||||||
|
if (subDir.endsWith("package.json")) continue;
|
||||||
|
// No need for Node10 resolution for component.config.ts
|
||||||
|
if (subDir.endsWith("convex.config.js")) continue;
|
||||||
|
// . just works with Node10 resolution
|
||||||
|
if (subDir === ".") continue;
|
||||||
|
console.log(subDir);
|
||||||
|
|
||||||
|
const newDir = path.join(baseDir, subDir);
|
||||||
|
const newPackageJsonPath = path.join(newDir, "package.json");
|
||||||
|
|
||||||
|
if (cleanup) {
|
||||||
|
try {
|
||||||
|
await fs.rm(newDir, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to remove ${newDir}:`, error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newPackageJson = {
|
||||||
|
main: `../dist/commonjs/${subDir}/index.js`,
|
||||||
|
module: `../dist/esm/${subDir}/index.js`,
|
||||||
|
types: `../dist/commonjs/${subDir}/index.d.ts`,
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.mkdir(newDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
newPackageJsonPath,
|
||||||
|
JSON.stringify(newPackageJson, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const isCleanup = process.argv.includes("--cleanup");
|
||||||
|
const isAddFiles = process.argv.includes("--addFiles");
|
||||||
|
const packageJsonPath = await findPackageJson(process.cwd());
|
||||||
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
|
||||||
|
|
||||||
|
if (!packageJson.exports) {
|
||||||
|
throw new Error("exports not found in package.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAddFiles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await processSubPackages(packageJsonPath, packageJson.exports, isCleanup);
|
||||||
|
|
||||||
|
if (isCleanup) {
|
||||||
|
console.log(
|
||||||
|
"Node10 module resolution compatibility stub directories removed.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"Node10 module resolution compatibility stub directories created",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
94
package.json
Normal file
94
package.json
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"name": "@convex-dev/polar",
|
||||||
|
"description": "A Polar component for Convex.",
|
||||||
|
"repository": "github:get-convex/polar",
|
||||||
|
"homepage": "https://github.com/get-convex/polar#readme",
|
||||||
|
"bugs": {
|
||||||
|
"email": "support@convex.dev",
|
||||||
|
"url": "https://github.com/get-convex/polar/issues"
|
||||||
|
},
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": [
|
||||||
|
"convex",
|
||||||
|
"component"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run build:esm && npm run build:cjs",
|
||||||
|
"build:esm": "tsc --project ./esm.json && echo '{\\n \"type\": \"module\"\\n}' > dist/esm/package.json",
|
||||||
|
"build:cjs": "tsc --project ./commonjs.json && echo '{\\n \"type\": \"commonjs\"\\n}' > dist/commonjs/package.json",
|
||||||
|
"dev": "cd example; npm run dev",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"prepare": "npm run build",
|
||||||
|
"prepack": "node node10stubs.mjs",
|
||||||
|
"postpack": "node node10stubs.mjs --cleanup",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
||||||
|
"test:coverage": "vitest run --coverage --coverage.reporter=text"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"@convex-dev/component-source": "./src/client/index.ts",
|
||||||
|
"types": "./dist/esm/client/index.d.ts",
|
||||||
|
"default": "./dist/esm/client/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"@convex-dev/component-source": "./src/client/index.ts",
|
||||||
|
"types": "./dist/commonjs/client/index.d.ts",
|
||||||
|
"default": "./dist/commonjs/client/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"./react": {
|
||||||
|
"import": {
|
||||||
|
"@convex-dev/component-source": "./src/react/index.ts",
|
||||||
|
"types": "./dist/esm/react.d.ts",
|
||||||
|
"default": "./dist/esm/react.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"@convex-dev/component-source": "./src/react/index.ts",
|
||||||
|
"types": "./dist/commonjs/react.d.ts",
|
||||||
|
"default": "./dist/commonjs/react.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"./convex.config": {
|
||||||
|
"import": {
|
||||||
|
"@convex-dev/component-source": "./src/component/convex.config.ts",
|
||||||
|
"types": "./dist/esm/component/convex.config.d.ts",
|
||||||
|
"default": "./dist/esm/component/convex.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"convex": "~1.16.5 || ~1.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"@types/node": "^18.17.0",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"convex-test": "^0.0.33",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"prettier": "3.2.5",
|
||||||
|
"typescript": "~5.0.3",
|
||||||
|
"typescript-eslint": "^8.4.0",
|
||||||
|
"vitest": "^2.1.4"
|
||||||
|
},
|
||||||
|
"main": "./dist/commonjs/client/index.js",
|
||||||
|
"types": "./dist/commonjs/client/index.d.ts",
|
||||||
|
"module": "./dist/esm/client/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@convex-dev/auth": "^0.0.74",
|
||||||
|
"@polar-sh/sdk": "^0.13.5",
|
||||||
|
"@react-email/components": "0.0.26",
|
||||||
|
"convex-helpers": "^0.1.63",
|
||||||
|
"standardwebhooks": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
242
src/client/index.ts
Normal file
242
src/client/index.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import {
|
||||||
|
Expand,
|
||||||
|
FunctionReference,
|
||||||
|
GenericDataModel,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
HttpRouter,
|
||||||
|
} from "convex/server";
|
||||||
|
import { GenericId } from "convex/values";
|
||||||
|
import { api } from "../component/_generated/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type WebhookSubscriptionCreatedPayload,
|
||||||
|
type WebhookSubscriptionCreatedPayload$Outbound,
|
||||||
|
WebhookSubscriptionCreatedPayload$inboundSchema as WebhookSubscriptionCreatedPayloadSchema,
|
||||||
|
} from "@polar-sh/sdk/models/components/webhooksubscriptioncreatedpayload";
|
||||||
|
import {
|
||||||
|
type WebhookSubscriptionUpdatedPayload,
|
||||||
|
type WebhookSubscriptionUpdatedPayload$Outbound,
|
||||||
|
WebhookSubscriptionUpdatedPayload$inboundSchema as WebhookSubscriptionUpdatedPayloadSchema,
|
||||||
|
} from "@polar-sh/sdk/models/components/webhooksubscriptionupdatedpayload";
|
||||||
|
import { Webhook } from "standardwebhooks";
|
||||||
|
import { internal } from "../component/_generated/api";
|
||||||
|
import type { Doc } from "../component/_generated/dataModel";
|
||||||
|
import { httpAction, type ActionCtx } from "../component/_generated/server";
|
||||||
|
import {
|
||||||
|
sendSubscriptionErrorEmail,
|
||||||
|
sendSubscriptionSuccessEmail,
|
||||||
|
} from "../component/email/templates/subscriptionEmail";
|
||||||
|
|
||||||
|
const handleUpdateSubscription = async (
|
||||||
|
ctx: ActionCtx,
|
||||||
|
user: Doc<"users">,
|
||||||
|
subscription:
|
||||||
|
| WebhookSubscriptionCreatedPayload
|
||||||
|
| WebhookSubscriptionUpdatedPayload
|
||||||
|
) => {
|
||||||
|
const subscriptionItem = subscription.data;
|
||||||
|
await ctx.runMutation(internal.lib.replaceSubscription, {
|
||||||
|
userId: user._id,
|
||||||
|
subscriptionPolarId: subscription.data.id,
|
||||||
|
input: {
|
||||||
|
productId: subscriptionItem.productId,
|
||||||
|
priceId: subscriptionItem.priceId,
|
||||||
|
interval: subscriptionItem.recurringInterval,
|
||||||
|
status: subscriptionItem.status,
|
||||||
|
currency: "usd",
|
||||||
|
currentPeriodStart: subscriptionItem.currentPeriodStart.getTime(),
|
||||||
|
currentPeriodEnd: subscriptionItem.currentPeriodEnd?.getTime(),
|
||||||
|
cancelAtPeriodEnd: subscriptionItem.cancelAtPeriodEnd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscriptionChange = async (
|
||||||
|
ctx: ActionCtx,
|
||||||
|
event: WebhookSubscriptionCreatedPayload | WebhookSubscriptionUpdatedPayload
|
||||||
|
) => {
|
||||||
|
const user = await ctx.runMutation(internal.lib.getsertUser, {
|
||||||
|
polarId: event.data.userId,
|
||||||
|
email: event.data.user.email,
|
||||||
|
});
|
||||||
|
if (!user?.email) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleUpdateSubscription(ctx, user, event);
|
||||||
|
|
||||||
|
const freePlan = await ctx.runQuery(internal.lib.getPlanByKey, {
|
||||||
|
key: "free",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only send email for paid plans
|
||||||
|
if (event.data.productId !== freePlan?.polarProductId) {
|
||||||
|
await sendSubscriptionSuccessEmail({
|
||||||
|
email: user.email,
|
||||||
|
subscriptionId: event.data.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePolarSubscriptionUpdatedError = async (
|
||||||
|
ctx: ActionCtx,
|
||||||
|
event: WebhookSubscriptionCreatedPayload | WebhookSubscriptionUpdatedPayload
|
||||||
|
) => {
|
||||||
|
const subscription = event.data;
|
||||||
|
|
||||||
|
const user = await ctx.runMutation(internal.lib.getsertUser, {
|
||||||
|
polarId: subscription.userId,
|
||||||
|
email: subscription.user.email,
|
||||||
|
});
|
||||||
|
if (!user?.email) throw new Error("User not found");
|
||||||
|
|
||||||
|
const freePlan = await ctx.runQuery(internal.lib.getPlanByKey, {
|
||||||
|
key: "free",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only send email for paid plans
|
||||||
|
if (event.data.productId !== freePlan?.polarProductId) {
|
||||||
|
await sendSubscriptionErrorEmail({
|
||||||
|
email: user.email,
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Response(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Polar {
|
||||||
|
constructor(public component: UseApi<typeof api>) {}
|
||||||
|
|
||||||
|
registerRoutes(http: HttpRouter) {
|
||||||
|
http.route({
|
||||||
|
path: "/polar/message-status",
|
||||||
|
method: "POST",
|
||||||
|
handler: httpAction(async (ctx, request) => {
|
||||||
|
if (!request.body) {
|
||||||
|
return new Response(null, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const wh = new Webhook(btoa(process.env.POLAR_WEBHOOK_SECRET!));
|
||||||
|
const body = await request.text();
|
||||||
|
const event = wh.verify(
|
||||||
|
body,
|
||||||
|
Object.fromEntries(request.headers.entries())
|
||||||
|
) as
|
||||||
|
| WebhookSubscriptionCreatedPayload$Outbound
|
||||||
|
| WebhookSubscriptionUpdatedPayload$Outbound;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.type) {
|
||||||
|
/**
|
||||||
|
* Occurs when a subscription has been created.
|
||||||
|
*/
|
||||||
|
case "subscription.created": {
|
||||||
|
return handleSubscriptionChange(
|
||||||
|
ctx,
|
||||||
|
WebhookSubscriptionCreatedPayloadSchema.parse(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occurs when a subscription has been updated.
|
||||||
|
* E.g. when a user upgrades or downgrades their plan.
|
||||||
|
*/
|
||||||
|
case "subscription.updated": {
|
||||||
|
return handleSubscriptionChange(
|
||||||
|
ctx,
|
||||||
|
WebhookSubscriptionUpdatedPayloadSchema.parse(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
switch (event.type) {
|
||||||
|
case "subscription.created": {
|
||||||
|
return handlePolarSubscriptionUpdatedError(
|
||||||
|
ctx,
|
||||||
|
WebhookSubscriptionCreatedPayloadSchema.parse(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "subscription.updated": {
|
||||||
|
return handlePolarSubscriptionUpdatedError(
|
||||||
|
ctx,
|
||||||
|
WebhookSubscriptionUpdatedPayloadSchema.parse(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
async add<Name extends string = keyof Shards & string>(
|
||||||
|
ctx: RunMutationCtx,
|
||||||
|
name: Name,
|
||||||
|
count: number = 1
|
||||||
|
) {
|
||||||
|
const shards = this.options?.shards?.[name] ?? this.options?.defaultShards;
|
||||||
|
return ctx.runMutation(this.component.lib.add, {
|
||||||
|
name,
|
||||||
|
count,
|
||||||
|
shards,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async count<Name extends string = keyof Shards & string>(
|
||||||
|
ctx: RunQueryCtx,
|
||||||
|
name: Name
|
||||||
|
) {
|
||||||
|
return ctx.runQuery(this.component.lib.count, { name });
|
||||||
|
}
|
||||||
|
// Another way of exporting functionality
|
||||||
|
for<Name extends string = keyof Shards & string>(name: Name) {
|
||||||
|
return {
|
||||||
|
add: async (ctx: RunMutationCtx, count: number = 1) =>
|
||||||
|
this.add(ctx, name, count),
|
||||||
|
subtract: async (ctx: RunMutationCtx, count: number = 1) =>
|
||||||
|
this.add(ctx, name, -count),
|
||||||
|
inc: async (ctx: RunMutationCtx) => this.add(ctx, name, 1),
|
||||||
|
dec: async (ctx: RunMutationCtx) => this.add(ctx, name, -1),
|
||||||
|
count: async (ctx: RunQueryCtx) => this.count(ctx, name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type utils follow */
|
||||||
|
|
||||||
|
type RunQueryCtx = {
|
||||||
|
runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
|
||||||
|
};
|
||||||
|
type RunMutationCtx = {
|
||||||
|
runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OpaqueIds<T> =
|
||||||
|
T extends GenericId<infer _T>
|
||||||
|
? string
|
||||||
|
: T extends (infer U)[]
|
||||||
|
? OpaqueIds<U>[]
|
||||||
|
: T extends object
|
||||||
|
? { [K in keyof T]: OpaqueIds<T[K]> }
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export type UseApi<API> = Expand<{
|
||||||
|
[mod in keyof API]: API[mod] extends FunctionReference<
|
||||||
|
infer FType,
|
||||||
|
"public",
|
||||||
|
infer FArgs,
|
||||||
|
infer FReturnType,
|
||||||
|
infer FComponentPath
|
||||||
|
>
|
||||||
|
? FunctionReference<
|
||||||
|
FType,
|
||||||
|
"internal",
|
||||||
|
OpaqueIds<FArgs>,
|
||||||
|
OpaqueIds<FReturnType>,
|
||||||
|
FComponentPath
|
||||||
|
>
|
||||||
|
: UseApi<API[mod]>;
|
||||||
|
}>;
|
||||||
68
src/component/_generated/api.d.ts
vendored
Normal file
68
src/component/_generated/api.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as email_index from "../email/index.js";
|
||||||
|
import type * as email_templates_subscriptionEmail from "../email/templates/subscriptionEmail.js";
|
||||||
|
import type * as init from "../init.js";
|
||||||
|
import type * as lib from "../lib.js";
|
||||||
|
import type * as polar from "../polar.js";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiFromModules,
|
||||||
|
FilterApi,
|
||||||
|
FunctionReference,
|
||||||
|
} from "convex/server";
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
declare const fullApi: ApiFromModules<{
|
||||||
|
"email/index": typeof email_index;
|
||||||
|
"email/templates/subscriptionEmail": typeof email_templates_subscriptionEmail;
|
||||||
|
init: typeof init;
|
||||||
|
lib: typeof lib;
|
||||||
|
polar: typeof polar;
|
||||||
|
}>;
|
||||||
|
export type Mounts = {
|
||||||
|
lib: {
|
||||||
|
getOnboardingCheckoutUrl: FunctionReference<
|
||||||
|
"action",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
polarAccessToken: string;
|
||||||
|
successUrl: string;
|
||||||
|
userEmail: string;
|
||||||
|
userId: string;
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
listPlans: FunctionReference<"query", "public", {}, any>;
|
||||||
|
setSubscriptionPending: FunctionReference<"mutation", "public", any, any>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// For now fullApiWithMounts is only fullApi which provides
|
||||||
|
// jump-to-definition in component client code.
|
||||||
|
// Use Mounts for the same type without the inference.
|
||||||
|
declare const fullApiWithMounts: typeof fullApi;
|
||||||
|
|
||||||
|
export declare const api: FilterApi<
|
||||||
|
typeof fullApiWithMounts,
|
||||||
|
FunctionReference<any, "public">
|
||||||
|
>;
|
||||||
|
export declare const internal: FilterApi<
|
||||||
|
typeof fullApiWithMounts,
|
||||||
|
FunctionReference<any, "internal">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export declare const components: {};
|
||||||
23
src/component/_generated/api.js
Normal file
23
src/component/_generated/api.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { anyApi, componentsGeneric } from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const api = anyApi;
|
||||||
|
export const internal = anyApi;
|
||||||
|
export const components = componentsGeneric();
|
||||||
60
src/component/_generated/dataModel.d.ts
vendored
Normal file
60
src/component/_generated/dataModel.d.ts
vendored
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(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>;
|
||||||
149
src/component/_generated/server.d.ts
vendored
Normal file
149
src/component/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/* 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 {
|
||||||
|
ActionBuilder,
|
||||||
|
AnyComponents,
|
||||||
|
HttpActionBuilder,
|
||||||
|
MutationBuilder,
|
||||||
|
QueryBuilder,
|
||||||
|
GenericActionCtx,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
GenericDatabaseReader,
|
||||||
|
GenericDatabaseWriter,
|
||||||
|
FunctionReference,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
type GenericCtx =
|
||||||
|
| GenericActionCtx<DataModel>
|
||||||
|
| GenericMutationCtx<DataModel>
|
||||||
|
| GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const query: QueryBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const mutation: MutationBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const action: ActionBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* This 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 action in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export declare const httpAction: HttpActionBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* This differs from the {@link MutationCtx} because all of the services are
|
||||||
|
* read-only.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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>;
|
||||||
90
src/component/_generated/server.js
Normal file
90
src/component/_generated/server.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/* 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 {
|
||||||
|
actionGeneric,
|
||||||
|
httpActionGeneric,
|
||||||
|
queryGeneric,
|
||||||
|
mutationGeneric,
|
||||||
|
internalActionGeneric,
|
||||||
|
internalMutationGeneric,
|
||||||
|
internalQueryGeneric,
|
||||||
|
componentsGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = 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 = 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 = 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 = 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 = 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 = internalActionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a Convex HTTP action.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||||
|
* as its second.
|
||||||
|
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||||
|
*/
|
||||||
|
export const httpAction = httpActionGeneric;
|
||||||
3
src/component/convex.config.ts
Normal file
3
src/component/convex.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineComponent } from "convex/server";
|
||||||
|
|
||||||
|
export default defineComponent("polar");
|
||||||
55
src/component/email/index.ts
Normal file
55
src/component/email/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const ResendSuccessSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
const ResendErrorSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
statusCode: z.number(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
name: z.literal("UnknownError"),
|
||||||
|
message: z.literal("Unknown Error"),
|
||||||
|
statusCode: z.literal(500),
|
||||||
|
cause: z.any(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type SendEmailOptions = {
|
||||||
|
to: string | string[];
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function sendEmail(options: SendEmailOptions) {
|
||||||
|
const from =
|
||||||
|
process.env.RESEND_SENDER_EMAIL_AUTH ??
|
||||||
|
"Convex SaaS <onboarding@resend.dev>";
|
||||||
|
const email = { from, ...options };
|
||||||
|
|
||||||
|
const response = await fetch("https://api.resend.com/emails", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(email),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const parsedData = ResendSuccessSchema.safeParse(data);
|
||||||
|
|
||||||
|
if (response.ok && parsedData.success) {
|
||||||
|
return { status: "success", data: parsedData } as const;
|
||||||
|
}
|
||||||
|
const parsedErrorResult = ResendErrorSchema.safeParse(data);
|
||||||
|
if (parsedErrorResult.success) {
|
||||||
|
console.error(parsedErrorResult.data);
|
||||||
|
throw new Error(`Error sending email: ${parsedErrorResult.data.message}`);
|
||||||
|
}
|
||||||
|
console.error(data);
|
||||||
|
throw new Error("Error sending email");
|
||||||
|
}
|
||||||
143
src/component/email/templates/subscriptionEmail.tsx
Normal file
143
src/component/email/templates/subscriptionEmail.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import { render } from "@react-email/render";
|
||||||
|
import { sendEmail } from "../index";
|
||||||
|
|
||||||
|
type SubscriptionEmailOptions = {
|
||||||
|
email: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Templates.
|
||||||
|
*/
|
||||||
|
export function SubscriptionSuccessEmail({ email }: SubscriptionEmailOptions) {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>Successfully Subscribed to PRO</Preview>
|
||||||
|
<Body
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container style={{ margin: "0 auto", padding: "20px 0 48px" }}>
|
||||||
|
<Img
|
||||||
|
src={`${process.env.SITE_URL}/images/convex-logo-email.jpg`}
|
||||||
|
width="40"
|
||||||
|
height="37"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
||||||
|
Hello {email}!
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
||||||
|
Your subscription to PRO has been successfully processed.
|
||||||
|
<br />
|
||||||
|
We hope you enjoy the new features!
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
||||||
|
The <Link href={`${process.env.SITE_URL}`}>domain-name.com</Link>{" "}
|
||||||
|
team.
|
||||||
|
</Text>
|
||||||
|
<Hr style={{ borderColor: "#cccccc", margin: "20px 0" }} />
|
||||||
|
<Text style={{ color: "#8898aa", fontSize: "12px" }}>
|
||||||
|
200 domain-name.com
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubscriptionErrorEmail({ email }: SubscriptionEmailOptions) {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>Subscription Issue - Customer Support</Preview>
|
||||||
|
<Body
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container style={{ margin: "0 auto", padding: "20px 0 48px" }}>
|
||||||
|
<Img
|
||||||
|
src="https://react-email-demo-ijnnx5hul-resend.vercel.app/static/vercel-logo.png"
|
||||||
|
width="40"
|
||||||
|
height="37"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
||||||
|
Hello {email}.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
||||||
|
We were unable to process your subscription to PRO tier.
|
||||||
|
<br />
|
||||||
|
But don't worry, we'll not charge you anything.
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
||||||
|
The <Link href={`${process.env.SITE_URL}`}>domain-name.com</Link>{" "}
|
||||||
|
team.
|
||||||
|
</Text>
|
||||||
|
<Hr style={{ borderColor: "#cccccc", margin: "20px 0" }} />
|
||||||
|
<Text style={{ color: "#8898aa", fontSize: "12px" }}>
|
||||||
|
200 domain-name.com
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders.
|
||||||
|
*/
|
||||||
|
export function renderSubscriptionSuccessEmail(args: SubscriptionEmailOptions) {
|
||||||
|
return render(<SubscriptionSuccessEmail {...args} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSubscriptionErrorEmail(args: SubscriptionEmailOptions) {
|
||||||
|
return render(<SubscriptionErrorEmail {...args} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Senders.
|
||||||
|
*/
|
||||||
|
export async function sendSubscriptionSuccessEmail({
|
||||||
|
email,
|
||||||
|
subscriptionId,
|
||||||
|
}: SubscriptionEmailOptions) {
|
||||||
|
const html = await renderSubscriptionSuccessEmail({ email, subscriptionId });
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: "Successfully Subscribed to PRO",
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendSubscriptionErrorEmail({
|
||||||
|
email,
|
||||||
|
subscriptionId,
|
||||||
|
}: SubscriptionEmailOptions) {
|
||||||
|
const html = await renderSubscriptionErrorEmail({ email, subscriptionId });
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: "Subscription Issue - Customer Support",
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
}
|
||||||
130
src/component/init.ts
Normal file
130
src/component/init.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { Polar } from "@polar-sh/sdk";
|
||||||
|
import { asyncMap } from "convex-helpers";
|
||||||
|
import { internal } from "./_generated/api";
|
||||||
|
import { internalAction, internalMutation } from "./_generated/server";
|
||||||
|
import schema, { CURRENCIES, INTERVALS, PlanKey, PLANS } from "./schema";
|
||||||
|
|
||||||
|
const seedProducts = [
|
||||||
|
{
|
||||||
|
key: PLANS.FREE,
|
||||||
|
name: "Free",
|
||||||
|
description: "Some of the things, free forever.",
|
||||||
|
amountType: "free",
|
||||||
|
prices: {
|
||||||
|
[INTERVALS.MONTH]: {
|
||||||
|
[CURRENCIES.USD]: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: PLANS.PRO,
|
||||||
|
name: "Pro",
|
||||||
|
description: "All the things for one low monthly price.",
|
||||||
|
amountType: "fixed",
|
||||||
|
prices: {
|
||||||
|
[INTERVALS.MONTH]: {
|
||||||
|
[CURRENCIES.USD]: 2000,
|
||||||
|
},
|
||||||
|
[INTERVALS.YEAR]: {
|
||||||
|
[CURRENCIES.USD]: 20000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const insertSeedPlan = internalMutation({
|
||||||
|
args: schema.tables.plans.validator,
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.insert("plans", {
|
||||||
|
polarProductId: args.polarProductId,
|
||||||
|
key: args.key,
|
||||||
|
name: args.name,
|
||||||
|
description: args.description,
|
||||||
|
prices: args.prices,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const seedProductsAction = internalAction({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
/**
|
||||||
|
* Stripe Products.
|
||||||
|
*/
|
||||||
|
const polar = new Polar({
|
||||||
|
server: "sandbox",
|
||||||
|
accessToken: process.env.POLAR_ACCESS_TOKEN,
|
||||||
|
});
|
||||||
|
const products = await polar.products.list({
|
||||||
|
organizationId: process.env.POLAR_ORGANIZATION_ID,
|
||||||
|
isArchived: false,
|
||||||
|
});
|
||||||
|
if (products?.result?.items?.length) {
|
||||||
|
console.info("🏃♂️ Skipping Polar products creation and seeding.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncMap(seedProducts, async (product) => {
|
||||||
|
// Create Polar product.
|
||||||
|
const polarProduct = await polar.products.create({
|
||||||
|
organizationId: process.env.POLAR_ORGANIZATION_ID,
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
prices: Object.entries(product.prices).map(([interval, amount]) => ({
|
||||||
|
amountType: product.amountType,
|
||||||
|
priceAmount: amount.usd,
|
||||||
|
recurringInterval: interval,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
const monthPrice = polarProduct.prices.find(
|
||||||
|
(price) =>
|
||||||
|
price.type === "recurring" &&
|
||||||
|
price.recurringInterval === INTERVALS.MONTH
|
||||||
|
);
|
||||||
|
const yearPrice = polarProduct.prices.find(
|
||||||
|
(price) =>
|
||||||
|
price.type === "recurring" &&
|
||||||
|
price.recurringInterval === INTERVALS.YEAR
|
||||||
|
);
|
||||||
|
|
||||||
|
await ctx.runMutation(internal.init.insertSeedPlan, {
|
||||||
|
polarProductId: polarProduct.id,
|
||||||
|
key: product.key as PlanKey,
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
prices: {
|
||||||
|
...(!monthPrice
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
month: {
|
||||||
|
usd: {
|
||||||
|
polarId: monthPrice?.id,
|
||||||
|
amount:
|
||||||
|
monthPrice.amountType === "fixed"
|
||||||
|
? monthPrice.priceAmount
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(!yearPrice
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
year: {
|
||||||
|
usd: {
|
||||||
|
polarId: yearPrice?.id,
|
||||||
|
amount:
|
||||||
|
yearPrice.amountType === "fixed"
|
||||||
|
? yearPrice.priceAmount
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info("📦 Polar Products have been successfully created.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { seedProductsAction as seedProducts };
|
||||||
275
src/component/lib.ts
Normal file
275
src/component/lib.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||||
|
import { Polar } from "@polar-sh/sdk";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
import { internal } from "./_generated/api";
|
||||||
|
import {
|
||||||
|
action,
|
||||||
|
internalAction,
|
||||||
|
internalMutation,
|
||||||
|
internalQuery,
|
||||||
|
mutation,
|
||||||
|
query,
|
||||||
|
} from "./_generated/server";
|
||||||
|
import schema from "./schema";
|
||||||
|
|
||||||
|
const createCheckout = async ({
|
||||||
|
polarAccessToken,
|
||||||
|
customerEmail,
|
||||||
|
productPriceId,
|
||||||
|
successUrl,
|
||||||
|
subscriptionId,
|
||||||
|
}: {
|
||||||
|
polarAccessToken: string;
|
||||||
|
customerEmail: string;
|
||||||
|
productPriceId: string;
|
||||||
|
successUrl: string;
|
||||||
|
subscriptionId?: string;
|
||||||
|
}) => {
|
||||||
|
const polar = new Polar({
|
||||||
|
server: "sandbox",
|
||||||
|
accessToken: polarAccessToken,
|
||||||
|
});
|
||||||
|
const result = await polar.checkouts.create({
|
||||||
|
productPriceId,
|
||||||
|
successUrl,
|
||||||
|
customerEmail,
|
||||||
|
subscriptionId,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlanByKey = internalQuery({
|
||||||
|
args: {
|
||||||
|
key: schema.tables.plans.validator.fields.key,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.db
|
||||||
|
.query("plans")
|
||||||
|
.withIndex("key", (q) => q.eq("key", args.key))
|
||||||
|
.unique();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUserSubscription = internalQuery({
|
||||||
|
args: {
|
||||||
|
userId: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await ctx.db
|
||||||
|
.query("users")
|
||||||
|
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
||||||
|
.unique();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
const subscription = await ctx.db
|
||||||
|
.query("subscriptions")
|
||||||
|
.withIndex("userId", (q) => q.eq("userId", user._id))
|
||||||
|
.unique();
|
||||||
|
if (!subscription) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const plan = await ctx.db.get(subscription.planId);
|
||||||
|
if (!plan) {
|
||||||
|
throw new Error("Plan not found");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...subscription,
|
||||||
|
plan,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getOnboardingCheckoutUrl = action({
|
||||||
|
args: {
|
||||||
|
polarAccessToken: v.string(),
|
||||||
|
successUrl: v.string(),
|
||||||
|
userId: v.string(),
|
||||||
|
userEmail: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const product = await ctx.runQuery(internal.lib.getPlanByKey, {
|
||||||
|
key: "free",
|
||||||
|
});
|
||||||
|
const price = product?.prices.month?.usd;
|
||||||
|
if (!price) {
|
||||||
|
throw new Error("Price not found");
|
||||||
|
}
|
||||||
|
const checkout = await createCheckout({
|
||||||
|
polarAccessToken: args.polarAccessToken,
|
||||||
|
customerEmail: args.userEmail,
|
||||||
|
productPriceId: price.polarId,
|
||||||
|
successUrl: args.successUrl,
|
||||||
|
});
|
||||||
|
return checkout.url;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getProOnboardingCheckoutUrl = internalAction({
|
||||||
|
args: {
|
||||||
|
interval: schema.tables.subscriptions.validator.fields.interval,
|
||||||
|
polarAccessToken: v.string(),
|
||||||
|
successUrl: v.string(),
|
||||||
|
userId: v.string(),
|
||||||
|
userEmail: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const product = await ctx.runQuery(internal.lib.getPlanByKey, {
|
||||||
|
key: "pro",
|
||||||
|
});
|
||||||
|
const price =
|
||||||
|
args.interval === "month"
|
||||||
|
? product?.prices.month?.usd
|
||||||
|
: product?.prices.year?.usd;
|
||||||
|
if (!price) {
|
||||||
|
throw new Error("Price not found");
|
||||||
|
}
|
||||||
|
const subscription = await ctx.runQuery(internal.lib.getUserSubscription, {
|
||||||
|
userId: args.userId,
|
||||||
|
});
|
||||||
|
const checkout = await createCheckout({
|
||||||
|
polarAccessToken: args.polarAccessToken,
|
||||||
|
customerEmail: args.userEmail,
|
||||||
|
productPriceId: price.polarId,
|
||||||
|
successUrl: args.successUrl,
|
||||||
|
subscriptionId: subscription?.polarId,
|
||||||
|
});
|
||||||
|
return checkout.url;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listPlans = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const userId = await getAuthUserId(ctx);
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
const plans = await ctx.db.query("plans").collect();
|
||||||
|
return plans.sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getsertUser = internalMutation({
|
||||||
|
args: {
|
||||||
|
polarId: v.string(),
|
||||||
|
email: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const getUser = () =>
|
||||||
|
ctx.db
|
||||||
|
.query("users")
|
||||||
|
.withIndex("polarId", (q) => q.eq("polarId", args.polarId))
|
||||||
|
.unique();
|
||||||
|
const existingUser = await getUser();
|
||||||
|
if (!existingUser) {
|
||||||
|
await ctx.db.insert("users", {
|
||||||
|
email: args.email,
|
||||||
|
polarId: args.polarId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const user = existingUser || (await getUser());
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
const subscription = await ctx.db
|
||||||
|
.query("subscriptions")
|
||||||
|
.withIndex("userId", (q) => q.eq("userId", user._id))
|
||||||
|
.unique();
|
||||||
|
if (!subscription) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
const plan = await ctx.db.get(subscription.planId);
|
||||||
|
if (!plan) {
|
||||||
|
throw new Error("Plan not found");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
subscription,
|
||||||
|
plan,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const replaceSubscription = internalMutation({
|
||||||
|
args: {
|
||||||
|
userId: v.id("users"),
|
||||||
|
subscriptionPolarId: v.string(),
|
||||||
|
input: v.object({
|
||||||
|
currency: schema.tables.subscriptions.validator.fields.currency,
|
||||||
|
productId: v.string(),
|
||||||
|
priceId: v.string(),
|
||||||
|
interval: schema.tables.subscriptions.validator.fields.interval,
|
||||||
|
status: v.string(),
|
||||||
|
currentPeriodStart: v.number(),
|
||||||
|
currentPeriodEnd: v.optional(v.number()),
|
||||||
|
cancelAtPeriodEnd: v.optional(v.boolean()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const subscription = await ctx.db
|
||||||
|
.query("subscriptions")
|
||||||
|
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
||||||
|
.unique();
|
||||||
|
if (subscription) {
|
||||||
|
await ctx.db.delete(subscription._id);
|
||||||
|
}
|
||||||
|
const plan = await ctx.db
|
||||||
|
.query("plans")
|
||||||
|
.withIndex("polarProductId", (q) =>
|
||||||
|
q.eq("polarProductId", args.input.productId)
|
||||||
|
)
|
||||||
|
.unique();
|
||||||
|
if (!plan) {
|
||||||
|
throw new Error("Plan not found");
|
||||||
|
}
|
||||||
|
await ctx.db.insert("subscriptions", {
|
||||||
|
userId: args.userId,
|
||||||
|
planId: plan._id,
|
||||||
|
polarId: args.subscriptionPolarId,
|
||||||
|
polarPriceId: args.input.priceId,
|
||||||
|
interval: args.input.interval,
|
||||||
|
status: args.input.status,
|
||||||
|
currency: args.input.currency,
|
||||||
|
currentPeriodStart: args.input.currentPeriodStart,
|
||||||
|
currentPeriodEnd: args.input.currentPeriodEnd,
|
||||||
|
cancelAtPeriodEnd: args.input.cancelAtPeriodEnd,
|
||||||
|
});
|
||||||
|
const user = await ctx.db.get(args.userId);
|
||||||
|
if (!user?.polarSubscriptionPendingId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ctx.scheduler.cancel(user.polarSubscriptionPendingId);
|
||||||
|
await ctx.db.patch(args.userId, {
|
||||||
|
polarSubscriptionPendingId: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setSubscriptionPending = mutation({
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
const userId = await getAuthUserId(ctx);
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
const scheduledFunctionId = await ctx.scheduler.runAfter(
|
||||||
|
1000 * 120,
|
||||||
|
internal.lib.unsetSubscriptionPending,
|
||||||
|
{ userId }
|
||||||
|
);
|
||||||
|
await ctx.db.patch(userId, {
|
||||||
|
polarSubscriptionPendingId: scheduledFunctionId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unsetSubscriptionPending = internalMutation({
|
||||||
|
args: {
|
||||||
|
userId: v.id("users"),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.patch(args.userId, {
|
||||||
|
polarSubscriptionPendingId: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
4
src/component/polar.ts
Normal file
4
src/component/polar.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Polar } from "@convex-dev/polar";
|
||||||
|
import { components } from "./_generated/api";
|
||||||
|
|
||||||
|
export const polar = new Polar(components.polar);
|
||||||
77
src/component/schema.ts
Normal file
77
src/component/schema.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
|
import { Infer, v } from "convex/values";
|
||||||
|
|
||||||
|
export const CURRENCIES = {
|
||||||
|
USD: "usd",
|
||||||
|
EUR: "eur",
|
||||||
|
} as const;
|
||||||
|
export const currencyValidator = v.union(
|
||||||
|
v.literal(CURRENCIES.USD),
|
||||||
|
v.literal(CURRENCIES.EUR)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const INTERVALS = {
|
||||||
|
MONTH: "month",
|
||||||
|
YEAR: "year",
|
||||||
|
} as const;
|
||||||
|
export const intervalValidator = v.union(
|
||||||
|
v.literal(INTERVALS.MONTH),
|
||||||
|
v.literal(INTERVALS.YEAR)
|
||||||
|
);
|
||||||
|
|
||||||
|
const priceValidator = v.object({
|
||||||
|
polarId: v.string(),
|
||||||
|
amount: v.number(),
|
||||||
|
});
|
||||||
|
const pricesValidator = v.object({
|
||||||
|
[CURRENCIES.USD]: v.optional(priceValidator),
|
||||||
|
[CURRENCIES.EUR]: v.optional(priceValidator),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PLANS = {
|
||||||
|
FREE: "free",
|
||||||
|
PRO: "pro",
|
||||||
|
} as const;
|
||||||
|
export const planKeyValidator = v.union(
|
||||||
|
v.literal(PLANS.FREE),
|
||||||
|
v.literal(PLANS.PRO)
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PlanKey = Infer<typeof planKeyValidator>;
|
||||||
|
|
||||||
|
export default defineSchema({
|
||||||
|
users: defineTable({
|
||||||
|
userId: v.optional(v.string()),
|
||||||
|
polarId: v.string(),
|
||||||
|
email: v.string(),
|
||||||
|
polarSubscriptionPendingId: v.optional(v.id("_scheduled_functions")),
|
||||||
|
})
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("polarId", ["polarId"]),
|
||||||
|
plans: defineTable({
|
||||||
|
key: planKeyValidator,
|
||||||
|
polarProductId: v.string(),
|
||||||
|
name: v.string(),
|
||||||
|
description: v.string(),
|
||||||
|
prices: v.object({
|
||||||
|
[INTERVALS.MONTH]: v.optional(pricesValidator),
|
||||||
|
[INTERVALS.YEAR]: v.optional(pricesValidator),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.index("key", ["key"])
|
||||||
|
.index("polarProductId", ["polarProductId"]),
|
||||||
|
subscriptions: defineTable({
|
||||||
|
planId: v.id("plans"),
|
||||||
|
polarId: v.string(),
|
||||||
|
polarPriceId: v.string(),
|
||||||
|
currency: currencyValidator,
|
||||||
|
interval: intervalValidator,
|
||||||
|
status: v.string(),
|
||||||
|
currentPeriodStart: v.optional(v.number()),
|
||||||
|
currentPeriodEnd: v.optional(v.number()),
|
||||||
|
cancelAtPeriodEnd: v.optional(v.boolean()),
|
||||||
|
userId: v.id("users"),
|
||||||
|
})
|
||||||
|
.index("userId", ["userId"])
|
||||||
|
.index("polarId", ["polarId"]),
|
||||||
|
});
|
||||||
8
src/react/index.ts
Normal file
8
src/react/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// This is where React components go.
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
throw new Error("this is frontend code, but it's running somewhere else!");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subtract(a: number, b: number): number {
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ES2021", "dom", "DOM.Iterable"],
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user