diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..31ab527d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,66 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy VitePress site to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: | + npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + touch docs/.vitepress/dist/.nojekyll + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a3e3973f..8f1fbd42 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ logs dist **/node_modules *.zip -ving/drizzle/migrations \ No newline at end of file +ving/drizzle/migrations +docs/.vitepress/cache +docs/.vitepress/dist diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js new file mode 100644 index 00000000..c4531306 --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,66 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Ving", + base: '/repo/', + description: "An opinionated Nuxt starter with restful API included", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + { text: 'Documentation', link: '/installation' } + ], + + sidebar: [ + { + text: 'Examples', + items: [ + { text: 'Markdown Examples', link: '/markdown-examples' }, + { text: 'Runtime API Examples', link: '/api-examples' } + ] + }, + { + text: 'Basics', + items: [ + { text: 'Installation', link: '/installation' }, + { text: 'Environment Variables', link: '/env' }, + { text: 'Error Codes', link: '/error-codes' }, + { text: 'Change Log', link: '/change-log' }, + ] + }, + { + text: 'Subsystems', + items: [ + { text: 'Cache', link: '/subsystems/cache' }, + { text: 'CLI', link: '/subsystems/cli' }, + { text: 'Drizzle', link: '/subsystems/drizzle' }, + { text: 'Email', link: '/subsystems/email' }, + { text: 'Jobs', link: '/subsystems/jobs' }, + { text: 'Logging', link: '/subsystems/logging' }, + { text: 'Message Bus', link: '/subsystems/messagebus' }, + { text: 'Pulumi', link: '/subsystems/pulumi' }, + { text: 'Rest', link: '/subsystems/rest' }, + { text: 'UI', link: '/subsystems/ui' }, + { text: 'Utilities', link: '/subsystems/utils' }, + { text: 'Ving Record', link: '/subsystems/ving-record' }, + { text: 'Ving Schema', link: '/subsystems/ving-schema' }, + ] + }, + { + text: 'Rest APIs', + items: [ + { text: 'APIKey', link: '/rest/APIKey' }, + { text: 'S3File', link: '/rest/S3File' }, + { text: 'Session', link: '/rest/Session' }, + { text: 'Test', link: '/rest/Test' }, + { text: 'User', link: '/rest/User' }, + ] + }, + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/plainblack/ving' } + ] + } +}) diff --git a/docs/APIKey.html b/docs/APIKey.html deleted file mode 100644 index ffb12238..00000000 --- a/docs/APIKey.html +++ /dev/null @@ -1,174 +0,0 @@ - - - -
- -Basics
- -Subsystems
-Rest APIs
- - -Developers use an API Key to create a Session via the Rest API and perform other functions that require validation.
-Prop | -Queryable | -Qualifier | -Range | -
---|---|---|---|
createdAt | -No | -No | -Yes | -
updatedAt | -No | -No | -Yes | -
name | -Yes | -No | -No | -
Name | -Record | -Type | -Endpoint | -
---|---|---|---|
user | -User | -Parent | -/api/apikey/:id/user | -
GET /api/apikey
-
-POST /api/apikey
-
-GET /api/apikey/:id
-
-PUT /api/apikey/:id
-
-DELETE /api/apikey/:id
-
-GET /api/apikey/options
-
-Basics
- -Subsystems
-Rest APIs
- - -S3File is the file upload system of Ving.
-The process of uploading a file happens in 3 steps:
-Here’s a bit more detail:
-Browser / Your Code --> POST filename and content type to /api/s3file
- * creates an S3File and sets its status to pending
- * generates a Presigned URL for S3
- <-- Return S3File description, including meta.presignedUrl
-
- --> PUT s3file.meta.presignedUrl
- * stores file in S3
- <-- Return nothing
-
- --> PUT s3file.props.id to an import API such as /api/user/:id/import-avatar
- * post processes the file uploaded to S3
- * verifies that the file conforms to the import rules
- <-- Return updated record such as User
-
-Prop | -Queryable | -Qualifier | -Range | -
---|---|---|---|
createdAt | -No | -No | -Yes | -
updatedAt | -No | -No | -Yes | -
filename | -Yes | -No | -No | -
sizeInBytes | -No | -Yes | -Yes | -
extension | -No | -Yes | -No | -
userId | -No | -Yes | -No | -
Name | -Record | -Type | -Endpoint | -
---|---|---|---|
user | -User | -Parent | -/api/s3file/:id/user | -
avatarUsers | -User | -Child | -/api/s3file/:id/avatarusers | -
GET /api/s3file
-
-POST /api/s3file
-
-You won’t actually post the file here. You post the filename
, contentType
, and sizeInBytes
here and it will return a presignedUrl
in the meta
section.
GET /api/s3file/:id
-
-PUT /api/s3file/:id
-
-DELETE /api/s3file/:id
-
-GET /api/s3file/options
-
-Basics
- -Subsystems
-Rest APIs
- - -In order to access privileged data you’ll on any ving endpoint you’ll need to pass a session id via an HTTP header cookie.
-Name | -Record | -Type | -Endpoint | -
---|---|---|---|
user | -User | -Parent | -/api/apikey/:id/user | -
POST /api/session
-
-{
- "apiKey" : "1b8e4f16-08ca-4829-befe-865cec37679b",
- "privateKey" : "pk_fd17bc887c7a00a1fffcb06a97961806616e"
-}
-
-GET /api/session/:id
-Cookie: vingSessionId=xxx
-
-DELETE /api/session/:id
-Cookie: vingSessionId=xxx
-
-Or
-DELETE /api/session
-Cookie: vingSessionId=xxx
-
-Basics
- -Subsystems
-Rest APIs
- - -Use this endpoint to test that you can post query params and body params to a ving service.
-GET /api/test
-
-POST /api/test
-
-PUT /api/test
-
-DELETE /api/test
-
-Any query params you post will be returned to you in the JSON response.
-On POST
and PUT
endpoints any body params will be returned to you in the JSON response.
{
- "success": true,
- "serverTime": "2023-05-09T00:19:35.151Z",
- "httpMethod": "GET",
- "query": {
- "foo": "bar"
- }
-}
-
-Basics
- -Subsystems
-Rest APIs
- - -Users can own records in ving. Users have privileges to access various types of data and store login credentials.
-Prop | -Queryable | -Qualifier | -Range | -
---|---|---|---|
createdAt | -No | -Yes | -Yes | -
updatedAt | -No | -No | -Yes | -
username | -Yes | -No | -No | -
Yes | -No | -No | -|
realName | -Yes | -No | -No | -
admin | -No | -Yes | -No | -
developer | -No | -Yes | -No | -
Name | -Record | -Type | -Endpoint | -
---|---|---|---|
apikeys | -APIKey | -Child | -/api/user/:id/apikeys | -
avatar | -S3File | -Child | -/api/user/:id/avatar | -
GET /api/user
-
-POST /api/user
-
-{
- "username" : "adufresne",
- "realName" : "Andy Dufresne",
- "password" : "rock hammer",
- "email" : "andy@shawshank.prison"
-}
-
-GET /api/user/:id
-
-PUT /api/user/:id
-
-{
- "useAsDisplayName" : "realName"
-}
-
-DELETE /api/user/:id
-
-GET /api/user/options
-
-Returns a user record for the currently logged in user based upon the session passed.
-GET /api/user/whoami
-Cookie: vingSessionId=xxx
-
-Attach an uploaded S3File to this user as an avatar.
-PUT /api/user/:id/import-avatar
-Cookie: vingSessionId=xxx
-
-{
- "s3FileId" : "xxx",
-}
-
-{{ theme }}+ +### Page Data +
{{ page }}+ +### Page Frontmatter +
{{ frontmatter }}+``` + + + +## Results + +### Theme Data +
{{ theme }}+ +### Page Data +
{{ page }}+ +### Page Frontmatter +
{{ frontmatter }}+ +## More + +Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/cache.html b/docs/cache.html deleted file mode 100644 index f5b625ee..00000000 --- a/docs/cache.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - -
Basics
- -Subsystems
-Rest APIs
- - -By default ving uses an in-memory cache that goes away every time you restart your server. That’s fine for early development, but you get logged out everytime you restart your server, so you’re going to want to set up a Redis cache as soon as you can.
-To use a Redis cache, you must first have a Redis server. Then you simply need to add an entry in .env that points to your Redis server like so:
-VING_REDIS="redis://@localhost:6379"
-
-You can include a username and password in the URL like so:
-VING_REDIS="redis://user:pass@localhost:6379"
-
-You can access the cache via to CLI, but for the most of your work you’ll want to programatically access the cache. Here’s a quick code example to show you how it works:
-import {useCache} from '#ving/cache.mjs';
-const cache = useCache();
-await cache.set('foo', 'bar', 60 * 60 * 1000); // set `foo` in key `foo` for 1 hour
-const foo = await cache.get('foo'); // `bar`
-await cache.delete('foo'); // delete the value associated with `foo`
-
-Basics
- -Subsystems
-Rest APIs
- - -TODO: Once we start doing relases I’ll add that here.
-Basics
- -Subsystems
-Rest APIs
- - -The CLI or Command Line Interface allows you to perform administrative and development functions quickly and easily.
-You access it from your project root like this:
-./ving.mjs --help
-
-You can explore what commands are available by using the above command. It has a built-in help system to tell you how to use the commands.
-To get help on a specific command, like user management, you can type:
-./ving.mjs user --help
-
-Basics
- -Subsystems
-Rest APIs
- - -The database layer is controlled by Drizzle. Drizzle table definitions should be generated from your ving schema. Drizzle provides a convenient way to write SQL queries in Javascript. Drizzle’s table definitions keep track of changes to your database schema over time allowing it to automatically generate database change migrations.
-Migrations are files created to help you migrate changes from one version of your database to another. In some systems you have to manually write migrations. But in ving you don’t, thanks to our use of Drizzle.
-Drizzle can automatically generate database migrations based upon changes in the Drizzle table definitions. You run that command like this:
-./ving.mjs drizzle --prepare
-
-Drizzle can automatically apply migrations to your database by running this command:
-./ving.mjs drizzle --up
-
-Normally you shouldn’t have to write many queries as ving records should handle a lot of that for you. But if you write complex backends like we do then inevitably you’ll need to write some.
-Writing Drizzle queries looks a lot like how you would write them with SQL, only in Javascript. You probably want to check out the official Drizzle documentation.
-We’ve exported a list of the useful drizzle utilities into a single file called #ving/drizzle/orm.mjs
. Below is an example of how you might use this:
import {eq} from '#ving/drizzle/orm.mjs';
-import {UsersTable} from '#ving/drizzle/schema/User.mjs';
-import {useDB} from '#ving/drizzle/db.mjs';
-
-const db = useDB()
-const result = await db.select().from(UsersTable).where(eq(UsersTable.email, 'joe@example.com'));
-
-Or if you are using ving records then its even easier:
-import {useKind} from '#ving/record/VingRecord.mjs';
-import {eq} from '#ving/drizzle/orm.mjs';
-
-const users = await useKind('User');
-const result = await users.select.where(eq(Users.table.email, 'joe@example.com'));
-
-If you are running queries against extremely large datasets and don’t want to load all that data into memory at once, we’ve partnered with the Drizzle Team to implement an asynchronous iterator that you can use. It works like this:
-const iterator = await db.select().from(UsersTable).iterator();
-
-for await (const row of iterator) {
- console.log(row);
-}
-
-This returns row results, not VingRecord
instances. However, the findaAll()
method in VingRecord
does it with records:
const allUsers = await Users.findAll();
-for await (const user of allUsers) {
- console.log(user.get('id'));
-}
-
-You can enable logging by adding ?log=yes
to the end of your VING_MYSQL
url like so:
VING_MYSQL="mysql://ving:vingPass@localhost:3306/ving?log=yes"
-
-Queries will be logged at level debug
in the drizzle
topic.
Basics
- -Subsystems
-Rest APIs
- - -Ving’s email system works over SMTP and is defined using templates created via Nunjucks.
-Add the following to your .env
file to be able to send mail.
VING_SMTP="smtp://USER:PASS@SERVER:465/?tls=no&log=no"
-
-Replace USER
and PASS
and SERVER
with the SMTP username, password, and server name of your SMTP server. You can also change the port from 465 to whatever you like. If it uses TLS then set tls
to yes
, and if you want logging enabled you can set log
to yes
.
Add an environment variable to override all outbound emails to an email address of your chosing.
-VING_EMAIL_OVERRIDE="example@gmail.com"
-
-Normally you’d use this in your dev environment so that all your test users email you instead of whatever made up email addresses you might be using. You can add this to your .env
file.
You can test an email template using the CLI by typing:
-./ving.mjs email --to=you@gmail.com
-
-Add --preview
to the end if you’d like to display the template in a browser rather than getting the email sent to you.
Define a template in server/email/templates
. Each set of templates will go into its own subfolder in that directory, and needs 3 files: html.njk
, subject.njk
, and text.njk
. These can be generated via the CLI:
./ving.mjs email --create NotifyAboutSweepstakes
-
-That will generate a folder with the files you can edit: server/email/templates/notify-about-sweepstakes
In server/email/templates/_wrappers
you will find the wrappers for HTML and Text variants of the emails. You can modify those to adjust the default headers and footers applied to all emails.
import { sendMail } from '/ving/email/send.mjs';
-await sendMail('notify-about-sweepstakes', { // template name matches the folder name
- options: { to: user.get('email') },
- vars: {
- foo: 'bar', // put whatever you like here
- color: 'red'
- }
-})
-
-Basics
- -Subsystems
-Rest APIs
- - -Everything that can be changed between various deployments of Ving from QA to Production to individual developer laptops is controlled in the .env
file in the root of the ving project folder. Thus .env
is excluded from git, as it should be different for each deployment. We then make use of these variables through Node’s --env-file
CLI parameter.
There are two types of variables that can end up in .env
, editable and generated variables.
Editable Variables are variables that you, the developer or devops person, are required to provide.
-VING_MYSQL="mysql://ving:fdsfdsfdsdsf@localhost:3380/ving"
-
-You can also add log=yes
to the end if you would like to see all SQL queries end up in your ving.log for debugging purposes.
VING_MYSQL="mysql://ving:ksdksdfllsdf@localhost:3380/ving?log=yes"
-
-The configuration URL for your SMTP server.
-VING_SMTP="smtp://AKIAdsfadasfTZZD:BadsfsdfsdsLL@email-smtp.us-east-1.amazonaws.com:465/?tls=yes&log=yes"
-
-The tls
and log
sections on the end of the URL are optional. Remove the tls
section if your SMTP server doesn’t support TLS encrypted communication. And remove the log
section if you don’t want to put every email outcome into your ving.log.
If this exists, all outgoing emails will go to this email address instead of their intended address. This is of course great for development purposes, and should not be used in production.
-VING_EMAIL_OVERRIDE="mytestemail@gmail.com"
-
-A connection string to your Redis install.
-VING_REDIS="redis://localhost:6379"
-
-Can also include username and password like this:
-VING_REDIS="redis://user:pass@localhost:6379"
-
-The access key that will be used to deploy resources and access S3.
-AWS_ACCESS_KEY_ID="JKHHGJKLHHGKL"
-
-The secret access key for AWS that will be used to deploy resources and access S3.
-AWS_SECRET_ACCESS_KEY="afsdlkjlkafsjdlkjasfdkjlalskfasdf"
-
-The AWS region you want to deploy services in.
-AWS_REGION="us-east-1"
-
-The URL of this site as it should appear in generated URLs. Should not end in a /
.
VING_SITE_URL="http://localhost:3000"
-
-Generated variables are generated by one of Ving’s subsystems, usually Pulumi, for resources that are automatically configured and then must be referenced later.
-This is the location of the AWS Lambda function that post processes S3File uploads.
-VING_LAMBDA_PROCESS_UPLOADS_URL="https://yvgmjfp5c7aasfdgf3efqhy0vbkll.lambda-url.us-east-1.on.aws/"
-
-This is the name of the S3 bucket that will hold S3File uploads.
-VING_AWS_UPLOADS_BUCKET="ving-dev-uploads-ea6sd53e"
-
-The the IAM key that will be used to grant users access to upload a file to the VING_AWS_UPLOADS_BUCKET.
-VING_AWS_UPLOADS_KEY="dssasdflkjadfslasfdRE7IQ"
-
-The the IAM secret that will be used to grant users access to upload a file to the VING_AWS_UPLOADS_BUCKET.
-VING_AWS_UPLOADS_SECRET="sssljadsflkjalsdfslfNLzO"
-
-This is the name of the S3 bucket that will hold S3File uploads generated thumbnails.
-VING_AWS_THUMBNAILS_BUCKET="ving-dev-thumbnails-6dsd53cc"
-
-As a side note there is also a ving.json
file. That is used to store configurable data that does not change between modes of deployment. Usually you’ll set this data when you set up your project, and then likely will never change it again.
This section deals with data relating to the site.
-The email address that messages generated by the site should come from by default.
-"email": "info@example.com",
-
-The name of this site.
-"name": "ving",
-
-The relative path of a logo in your repo to display in emails and on the site.
-"logoUrl": "/ving.svg",
-
-An array containing the physical street address of the entity that owns the site. This is required for compliance with anti-spam laws.
-"streetAddress": [
- "123 Main Street",
- "Madison, WI 53701",
- "USA"
-]
-
-Basics
- -Subsystems
-Rest APIs
- - -This document describes the error codes thrown by Ving’s REST API. They map directly onto the W3C’s standard HTTP status codes. When functioning properly the web service will always return a 200 HTTP status code.
-NOTE: While the error codes documented here are returned as HTTP status codes, they are also returned in the JSON response of the body.
-The server cannot process the request because the request was malformed -or a prerequisite of performing the requested action has not been met.
-The session you are using has expired. Request a new one before continuing.
-For one reason or another the payment requested was declined. Usually due to typos, but could also do with credit card holds, insufficient funds, etc.
-You do not have the privileges necessary to complete that operation.
-The object you requested doesn’t exist. This refers to an object specified in the query string, not in the path.
-Whatever you requested took too long and the server gave up.
-The name or resource requested is already in use by someone else, or has already moved on to a new stage of it’s life so the function you are trying to perform on it is no longer valid.
-You tried to post something (perhaps upload a file) that is too large.
-You tried to assign a file to a field that doesn’t match the field’s criteria or you tried to upload a file that the system doesn’t allow. For example you tried to assign a PDF to a field looking for images.
-You have exceeded the maximum number of requests allowed per minute. This exception is telling you to slow down so you don’t denial of service the server with your requests.
-You’re missing a required parameter.
-The value specified for a field was out of range. If it’s a numeric field make sure you’re above the minumum and below the maximum. If it’s an enumerated field make sure you’ve specfified an valid option.
-The password you specified does not match our records.
-This request was going to take too long so it was handed off to be processed in the background.
-An unhandled exception has occurred in the server. Under normal operating procedures this should never happen, as all exceptions should be trapped within the code and returned as a defined exception. Therefore this is an untrapped exception, and is in all cases a bug.
-You have encountered a feature that is planned, but not yet implemented.
-An external resource returned a garbage response that caused your request to fail.
-Could not connect to an external resource, such as a database or web service.
-Basics
- -Subsystems
-Rest APIs
- - -Ving is a Web and REST code generation tool and services framework. It has a heavy focus on a strong and predictable backend, while having a flexible front end. It’s feature’s include:
-Ving is written entirely in Javascript using Nuxt 3, Vue 3, PrimeVue, PrimeFlex and Drizzle.
-Everything starts with the Ving Schema, which defines tables in a database, along with fields, privileges, and other properties. From there everything is automatically generated, but still modifyable by you. Ving will generate database schemas and migrations, server-side Javascript APIs, REST APIs, Web UIs, email templates, and more.
-You might be wondering why REST in a world with tRPC and GraphQL. It’s because REST is language agnostic, and very simple to understand. It’s still the best data presentation layer when you have an API you want the general public to consume. And Ving is all about being fast to implement and easy to maintain. When you want simplcity, you do not want GraphQL or Typescript as there’s nothing uncomplicated about either.
-You can visit the GitHub repository to get the source code and start using it.
-Basics
- -Subsystems
-Rest APIs
- - -You need to choose whether you want to be able to get updates from future versions of ving or not.
-Any modern computer should be able to run ving. But you will need to install a couple of things to get started.
-Choose this option so you can choose to sync the changes from the main ving repo into your repository at any time in the future.
- -After forking, clone your forked repo to your computer.
-Choose this option if you just want to use ving as a starter and don’t care about what happens in the future.
-Download a zip file of the repo.
-Then unzip it.
-Then feel free to rename the folder to whatever you are calling your project.
-mv ving my-cool-project
-
-Type the following:
-cd ving
-npm install
-
-For developement we recommend Visual Studio Code.
-And for the best possible experience, we also recommend installing these plugins:
-You’ll need to download and install a MySQL 8 database.
---You could convert ving to Postgres or any other supported Drizzle database, but that’s not on our todo list.
-
Log in to your MySQL database as the root user and then type the following:
-create database ving;
-CREATE USER 'ving'@'localhost' IDENTIFIED BY 'vingPass';
-grant all privileges on ving.* to 'ving'@'localhost';
-flush privileges;
-
---Obivously use your own username and password options, not the samples we provided here.
-
Create .env
in the project root and add your dev database connection string.
VING_MYSQL="mysql://ving:vingpass@localhost:3306/ving"
-
---Obivously modify the username, password, and database name to match what you created in the previous step.
-
Now you can create the initial tables into your database using the CLI.
-./ving.mjs drizzle --prepare
-./ving.mjs drizzle --up
-
-Also use the CLI to create a user so you can log in to the web interface.
-./ving.mjs user --add Admin --email you@domain.com --password 123qwe --admin
-
-Start the development server on http://localhost:3000
-npm run dev
-
-To use Redis as your cache, you’ll need to configure it first.
-Ving offers email sending and templating, but you need to configure an SMTP server first.
-If you want to make use of AWS for things like storing file uploads in S3, then you’ll also want to check out our Pulumi integration.
-Basics
- -Subsystems
-Rest APIs
- - -Ving uses a jobs server called BullMQ to execute potentially long running asynchronous background jobs.
-To use the jobs system you must first have a Redis server. Then you simply need to add an entry in .env that points to your Redis server like so:
-VING_REDIS="redis://@localhost:6379"
-
-You can include a username and password in the URL like so:
-VING_REDIS="redis://user:pass@localhost:6379"
-
-NOTE: You must enable the setting maxmemory-policy=noeviction
in your Redis server to prevent it from automatically deleting keys when memory runs low as this will cause problems with the BullMQ system.
To enqueue a job you’d use the following code.
-import ving from '#ving/index.mjs';
-
-ving.addJob('Test', { foo: 'bar' }, { delay: 1000 * 60 });
-
-That would run a handler called Test
with the parameter of { foo: 'bar' }
, but first it would wait 60 seconds before the job would be run.
You can also enqueue jobs from the CLI.
-./ving.mjs jobs --addJob Test --jobData '{ "foo": "bar" }'
-
-Jobs are run by job workers using handlers. The job worker system is run via the CLI.
-./ving.mjs --worker --ttl 60
-
-The above would run a worker for 60 seconds afterwhich the worker would shut down. If you want to run it indefinitely just leave the ttl
off.
At the heart of the jobs system are handlers. Handlers are custom code that knows how to process a job. You can find handlers in the #ving/jobs/handlers
folder. This is what a simple handler looks like:
import ving from '#ving/index.mjs';
-
-export default async function (job) {
- ving.log('jobs').debug(`Test ran with data: ${JSON.stringify(job.data)}`);
- return true;
-}
-
-The handler’s job is to do whatever needs to be done with job.data
to complete the job. It should return true
when done, or throw
an ouch if it fails.
Basics
- -Subsystems
-Rest APIs
- - -By default Ving logs to the logs
folder. It automatically rotates its own logs.
Ving uses Winston for logging. Winston has a lot of amazing configurability, so instead of creating a config file that cannot possibly encapsulate all of that, we leave it to you to modify server/log.mjs
.
To write to a log you’d do something like this:
-import ving from '#ving/index.mjs';
-ving.log('topic').error('Error message goes here.');
-
-The parameter going into the log()
function is there to set a topic (or category) for the log entry, and then the actual message to be logged would go inside one of the sub functions. Sub function are info()
, error()
, warn()
, or debug()
.
Basics
- -Subsystems
-Rest APIs
- - -Ving’s Message Bus push messages from the server to a logged in user via Server Sent Events (SSE). This can be useful for displaying a toast via the notification system, or for triggering some functionality when a background job finishes.
-If you’d like to test it, log in to your ving site and then from the CLI type:
-./ving.mjs messagebus -u Admin -m "Hello Admin!"
-
-Note that this functionality requires that you’ve set up the Redis cache.
-If you want to extend this functionality to send your own message types from the server to the browser, you’ll need to do the following 3 steps.
-In server/messagebus.mjs
add and export a function that will publish the message. Let’s say we’re going to update a progress bar somewhere for some background process. We’d create a publisher function for that like:
export const publishSomeProgressBar = async (
- userId,
- percentageComplete = 0,
- fullyComplete = 100
-) => {
- return publish(
- userId,
- 'someProgressBar',
- { percentageComplete, fullyComplete }
- );
-}
-
-Note that you can skip this step and use use the generic publish
function, but that doesn’t give you the opportunity to add defaults, an API, or error handling so we recommend creating a publish function.
Wherever in your code that you can get your event data to publish to the browser, use your newly created publisher function.
-import {publishSomeProgressBar} from '../server/messagebus.mjs'
-
-await publishSomeProgressBar(userId, 35);
-
-You’ll need to update composables/useMessageBus.mjs
to handle your someProgressBar
message type. Add your new case
to the switch
statement there.
Basics
- -Subsystems
-Rest APIs
- - -Pulumi is an open source Infrastructure as Code (IaC) system. It allows you to automate setting up servers and services. Ving uses it to make configuring AWS faster, rather than giving you a bunch of instructions to perform and hope you get them all right, we’ve programmed Pulumi to do it for you.
-Follow the Pulumi install instructions.
-If you have not already set up an AWS credentials file in ~/.aws/credentials
then you’ll want to do that now as pulumi will use that to log in to AWS.
Then edit the Pulumi.yaml
file and change the name
field from ving
to your project name. This name will be used to prefix all the generated services in AWS so you know what we made.
Then type pulumi up
at your command line. When it asks you about what stack you want, say dev
unless you already know how to use pulumi and want to use your own stack name. And once it shows you its plan for deployment, use your arrow key to move up to yes
to deploy it.
By editing Pulumi.mjs
you can add your own infrastructure automations. And then use pulumi up
to roll them out to the server. Just make sure you don’t remove or change any of the pulumi code that we have there unless you understand the implications.
Basics
- -Subsystems
-Rest APIs
- - -The restful web services that ving creates are not only used to drive the UI, but also are intended to be used by your fans, customers, and the general public to interact with your web site in an automated fashion. This is the core reason we generate REST endpoints rather than using something like tRPC.
-This document will tell you about the general premise of the Rest interface.
-Your Rest endpoints will be generated from your Ving Schema by using the CLI:
-./ving.mjs record --rest Foo
-
-These will be placed in the server/api/foo
folder and can be modified by you after the fact.
There are several conventions used in this documentation to keep things shorter. They are documented here.
-We often shorten pieces of return values with ellipsis (three dots: …) to show that there would be additional data there, but it is not directly relevent to the documentation at hand.
-ID’s are often represented as 3 x’s: xxx
. If you see xxx
anywhere that means that would be replaced by a legitimate ID and shouldn’t be interpreted literally. Also, ID’s are case-sensitive strings, so store them as such.
When referencing any API herein we omit the domain for the site. So you should prefix it with whatever site you are trying to access like:
-http://some.example.com/api/user/xxx
-
-To make a request to a Wing web service you need nothing more than a command line tool like curl
, or better yet use the VS Code Rest Client. You can of course use any network aware programming language as well. Here’s an example request using the VS Code Rest Client:
POST http://ving.example.com/api/article
-Content-Type: application/json
-
-{
- "title" : "Ethics in Prisons",
- "author" : "Andy Dufresne"
-}
-
-Response:
-{
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : ""
- }
-}
-
-GET http://wing.example.com/api/article/xxx
-
-Response:
-{
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : ""
- }
-}
-
-PUT http://ving.example.com/api/article/xxx
-Content-Type: application/json
-
-{
- "body" : "..."
-}
-
-Response:
- {
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : "..."
- }
- }
-
-DELETE http://ving.example.com/api/article/xxx?includeMeta=true
-
-Response
-
- {
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : "..."
- },
- "meta" : {
- "deleted" : true
- }
- }
-
-GET http://ving.example.com/api/article
-
-Response:
-{
- "paging": {
- "page": 1,
- "nextPage": 2,
- "previousPage": 1,
- "itemsPerPage": 10,
- "totalItems": 43,
- "totalPages": 5
- },
- "items" : [
- {
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : "..."
- }
- },
- ...
- ]
-}
-
-With each request you can vingSessionId
cookie header if you want to get an
-authenticated result. If you choose not to pass the vingSessionId
, then
-the result you receive will be the public result set. If you do pass the
-vingSessionId
then you’ll get the private result set (provided your session
-has the privileges to receive the private result set). For example, if you
-request information about your user account without specifying a vingSessionId
-then all you’d get back is an ID and some other basic information, like this:
GET http://wing.example.com/api/user/xxx?includeMeta=true
-
-Response:
-{
- "props" : {
- "id" : "xxx",
- "createdAt" : "2012-04-23T18:25:43.511Z",
- "createdAt" : "2023-04-25T08:13:10.001Z",
- ...
- },
- "meta" : {
- "displayName" : "Andy Dufresne",
- ...
- },
-}
-
-But if you request your account information with your vingSessionId
, then you’d
-get a result set with everything we know about you:
GET http://wing.example.com/api/user/xxx?includeMeta=true
-Cookie: vingSessionId="yyy"
-
-Response:
-{
- "props" : {
- "id" : "xxx",
- "createdAt" : "2012-04-23T18:25:43.511Z",
- "createdAt" : "2023-04-25T08:13:10.001Z",
- "realName" : "Andy Dufresne",
- "username" : "andy",
- "email" : "andy@shawshank.jail",
- "useAsDisplayName" : "realName",
- ...
- },
- "meta" : {
- "displayName" : "Andy Dufresne",
- ...
- },
-}
-
-However, if I requested information about your account, and specified my own
-vingSessionId
, then I would only get the public data. Because I don’t have
-the privileges necessary to access your private information.
A big part of the ving specification is that you can reliably expect it to do the same thing in all circumstances. Here are a few key points of consistency.
-All record objects contain the following minimum shared set of attributes.
-An object of database stored properties.
-A unique id that will never change. It is a 36 character GUID (global unique id).
-The date this record came into existence.
-The last time this record was written to the database.
-An object of links to API endpoints related to this record.
-A link to create records of this type or list all records in the system of this type.
-A link to get/delete this sepecfic record.
-An object of generated properties.
-A string representing the type of object this is in case you need to identify it in the future.
-Dates are always returned in the format of CYYYY-MM-DDTHH:MM:SS.mmmZ (the Javascript/JSON stringified date format) and are represented as the UTC time zone.
-Ving will always return a JSON response in the form of an object.
-{
- "props" : { "success" : 1 }
-}
-
-Results will always start with a top level object, and if its a Ving Record it will at minimum have a prop
object nested within it.
{
- "props" : {
- "id" : "xxx",
- ...
- }
-}
-
-Paginated lists are always handled exactly the same way, and always have the same minimum set of parameters for manipulation.
-GET /api/article?itemsPerPage=25&page=3
-
-You can tell how many items per page to return and which page number to return. That will give you a result set like this:
-{
- "paging": {
- "page": 3,
- "nextPage": 4,
- "previousPage": 2,
- "itemsPerPage": 25,
- "totalItems": 937,
- "totalPages": 38
- },
- "items" : [
- {
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : "..."
- }
- },
- ...
- ]
-}
-
-1
and 100
that defaults to 10
and represents how many items should be included per page.1
and 1000000
that defaults to 1
and represents the current page number of the result set.prop
of the record and defaults to createdAt
.asc
but could also be desc
if you want the order of the records to be sorted in descending order.1
and 100000000000
that defaults to 100000000000
that limits the total number of items that can ever be paginated through.Exceptions will always start with a top level element called error
and then will have an object of 3 properties: code
, message
, data
.
{
- "error" : {
- "code" : 500,
- "message" : "An unknown error has occurred.",
- "data" : null
- }
- }
-
-The code
is always an integer and conforms to the standard list of Error Codes. These numbers are used consistently so that app developers can trap and handle specific types of exceptions more gracefully.
The message
is a human readable message that you can display to a user.
The data
element many times will be null, but it can have important debug information. For example, if a required field was left empty, the field name could be put in the data element so that the app could highlight the field for the user.
In addition to exceptions there can be less severe issues that come up. These are handled via warnings. Warnings are just like exceptions, but they don’t cause execution to halt. As such there can be any number of warnings. And warnings are returned with the result.
- {
- "warnings" : [
- {
- "code" : 445,
- "message" : "Logo image is too big.",
- "data" : "logo"
- }
- ],
- ...
- }
-
-All objects can have relationships to each other. When you fetch an object, you can pass includeLinks=true
as a parameter if you want to get the relationship data as well.
GET /api/article/xxx?includeLinks=true
-
-Response:
- {
- "props" : {
- "id" : "xxx",
- ...
- },
- "links" : {
- "base" : {
- "href": "/api/user",
- "methods": ["GET","POST"]
- },
- "self" : {
- "href": "/api/user/xxx",
- "methods": ["GET","PUT","DELETE"]
- },
- "articles" : {
- "href": "/user/xxx/articles",
- "methods": ["GET"]
- },
- }
- }
- }
-
-You can then in-turn call the URI provided by each relationship to fetch the items in that list.
-Likewise you can request related objects (those with relationship type of parent) be included directly in the result by adding the name of the related record relationship like `includeRelated=user as a parameter:
-GET /article/xxx?includeRelated=user&includeMeta=true
-
-Response:
- {
- "props" : {
- "id" : "xxx",
- "author" : "Andy Dufresne",
- "title" : "Ethics in Prisons",
- "body" : "...",
- "userId" : "xxx",
- },
- "related" : {
- "user" : {
- "props" : {
- "id" : "xxx",
- ...
- },
- "meta" : {
- "displayName" : "Andy Dufresne",
- ...
- }
- }
- }
- }
-
-All related objects are also inherently relationships of the object. Therefore the documentation will leave them out of the list of relationships in each object, but will include them in the list of related objects.
---NOTE: The only related objects that can be returned in this manner are 1:1 relationships. If the relationship is 1:N as in the case of related articles above, then those cannot not be included in the result, and must be fetched separately.
-
Filters allow you to modify the result set when querying a list of records.
-Some relationships will allow you to use a search
parameter on the URL that will allow you to search the result set. The documentation will tell you when this is the case and which fields will be searched to provide you with a result set.
GET /api/article/xxx/related-articles?search=prison
-
-In search engines these are sometimes called facets. They are criteria that allow you to filter the result set by specific values of a specfic field. The documentation will tell you when a relationship has a qualifier. To use it you’d add a parameter of the name of the qualifier to the URL along with the value you want to search for.
-GET /api/article/xxx/related-articles?userId=xxx
-
-That will search for all related articles with a userId
of xxx
.
You can also modify the qualifier by prepending operators such as >
, >=
, <=
, and <>
(or !=
) onto the value. For example:
GET /api/article/xxx/related-articles?wordCount=>=100
-
-Get all related articles with a word count greater than or equal to 100
.
You can also request that a qualifier be limited to a null
value.
GET /api/article/xxx/related-articles?userId=null
-
-If you did this with an empty ''
or undefined
value rather than specifically null
then this qualifier will be skipped.
You can also use ranged filters to limit data that must fall between 2 values by prepending _start_
or _end_
to the name of the prop you wish to filter on range.
GET /api/article/xxx/related-articles \
- ?_start_createdAt=2012-04-23T18:25:43.511Z \
- &_end_createdAt=2023-04-25T08:13:10.001Z
-
-Some records will allow for extra includes, and will show this in the documenation. Extra includes are extra bits of data you can pull back when you request the record, that are unique to that record.
-GET /api/article/xxx/related-articles?includeExtra=foo
-
-The result will then have foo
in an extras
block like:
{
- "props" : {
- "id" : "xxx",
- ...
- }
- "extras" : {
- "foo" : {...}
- }
- }
-
-Sometimes a record will have fields that require you to choose an option from an enumerated list. There are two ways to see what those options are:
-This way would be most often used when you need the list of options in order to create a record.
-GET /api/article/options
-
-Response:
-{
- "bookType" : [
- {"label" : "Hardcover", "value" : "hard" },
- {"label" : "Paperback", "value" : "soft" }
- ],
- ...
-}
-
-This way would be most often used when you need the list of options to update an object, because you can get the properties of the object and the options in one call.
-GET http://wing.example.com/article/xxx?includeOptions=true
-
- {
- "props" : {
- "id" : "xxx",
- ...
- },
- "options" : {
- "bookType" : [
- {"label" : "Hardcover", "value" : "hard" },
- {"label" : "Paperback", "value" : "soft" }
- ],
- ...
- }
- }
-
-There is a composable built into ving called useVingRecord that will allow you to access the Rest API easily for individual records.
-There is a composable built into ving called useVingKind that will allow you to access the Rest API easily for lists of records. It uses the useVingRecord composable underneath to give you access to the individual records within the list.
-curl -X POST -d '{"title":"Ethics in Prisons","author":"Andy Dufresne"}' \
- -H Content-Type: application/json \
- -H Cookie: vingSessionId=yyy http://ving.example.com/api/article
-
-POST http://ving.example.com/api/article
-Content-Type: application/json
-Cookie: vingSessionId=yyy
-
-{
- "title" : "Ethics in Prisons",
- "author" : "Andy Dufresne"
-}
-
-If you don’t want to use an available client, but instead write your own, there is a Test API that can help make sure your client is working before you start using the real web service.
-Basics
- -Subsystems
-Rest APIs
- - -The web user interface of ving allows you to build out complex applications using Vue 3. It starts with automatically generating pages for your ving records. We use a component library suite called PrimeVue that provides all kinds of amazing functionality and a styling library called PrimeFlex that gives you rich power over CSS. But we’ve also got a bunch of custom components, composables, and stores to help you build your app.
-ving is ultimately built on Nuxt, so ving pages can do anything Nuxt Pages can do.
-You can automatically generate a set of pages for interacting with ving records through the Rest API by using the CLI like this:
-./ving.ts record --web Foo
-
-That will give you a place to start, and then you can use the composables, components, and stores we provide to build out a complex app.
-Displays the site-wide administrative navigation.
-<AdminNav :crumbs="breadcrumbs" />
-
-See Crumbtrail
for more info about the crumbs
prop.
Displays a crumbtrail navigation.
-<Crumbtrail :crumbs="breadcrumbs" />
-
-Props:
-Creates a user interface for uploading S3Files. It handles the resizing of images on the client side, restriction of file types on the client side, requesting the presigned upload URL, uploading the file to S3. The only thing you need to do is specify via afterUpload
what happens to the file after the user uploads it.
Note that you should always wrap this in a <client-only>
tag.
<client-only>
- <Dropzone :acceptedFiles="['.pdf','.zip']" :afterUpload="doThisFunc"></Dropzone>
-</client-only>
-
-Props:
-.
like .jpg
not jpg
. Defaults to ['.png','.jpg']
.100000000
.contain
, which is the default, and that means that the image will retain its aspect ratio, but will not exceed the bounds of resizeWidth
and resizeHeight
. The second option is crop
which will crop the middle portion of the image to the bounds of resizeWidth
and resizeHeight
and discard anything outside those bounds.jpg
and webp
images during upload. 0.6
would represent 60% quality. Defaults to 1
.A fieldset element within a FieldsetNav
.
<FieldsetItem name="Foo">
-Forms go here...
-</FieldsetItem>
-
-Props:
-An inline page nav for a large scrollable form to be divided up into sections using FieldsetItem
.
<FieldsetNav>
- <FieldsetItem name="Content">...</FieldsetItem>
- <FieldsetItem name="Taxonomy">...</FieldsetItem>
- <FieldsetItem name="Privileges">...</FieldsetItem>
-</FieldsetNav>
-
-A form element to allow coordination of validation of inputs.
-<Form :send="someFunc()">...</Form>
-
-Props:
-Generate the appropritate form field based upon input types.
-<FormInput name="username" v-model="user.username" />
-
-Props:
-text
but can also be textarea
, password
, number
, or email
.name
field is set to, but can be any string..00
$
off
.false
, but if true will not allow the form to be sent if empty.number
type field. Defaults to 1
.Password
.An ARIA compliant label for a form field.
-<FormLabel id="foo" label="Foo" />
-
-Props:
-A form select list.
-<FormSelect>
-
-Props:
-name
field.Place this in your layouts so that users can receive toasts that will be triggered via the useNotifyStore()
composable.
<client-only>
- <Notify/>
-</client-only>
-
-Displays a pagination bar for a useVingKind() result set.
-<Pager :kind="users" />
-
-Props:
-Place this in your layouts where you would like the system wide alert to be displayed when an admin has configured one. It is triggered by the useSystemWideAlertStore()
composable.
<client-only>
- <SystemWideAlert/>
-</client-only>
-
-Place this in your layouts so the user has an indication that there are some background activites such as rest calls happening. It is triggered by the useThrobberStore()
composable.
<client-only>
- <Throbber />
-</client-only>
-
-Navigation for user settings.
-<UserSettingsNav />
-
-Each of these also has documentation of how to use them in the form of JSDocs in the source code.
-Gets you the currently logged in user.
-const user = useCurrentUserStore();
-if (await user.isAuthenticated()) {
- // do logged in user stuff
-}
-
-It also triggers 2 window events for when the user logs in or out.
- window.addEventListener('ving-login', (event) => {
- // do something after login
- });
- window.addEventListener('ving-logout', (event) => {
- // do something after logout
- });
-
-Date formatting tools based upon date-fns.
-const dt = useDateTime()
-const date = dt.determineDate("2012-04-23T18:25:43.511Z");
-const formattedDateTime = dt.formateDateTime(new Date());
-const formattedDate = dt.formateDate(new Date());
-const timeago = dt.formatTimeAgo("2012-04-23T18:25:43.511Z");
-
-Connects the browser to the server’s message bus. It establishes a connection between your browser and the server, so it needs to be installed in an onMounted()
handler in your layouts.
onMounted(() => {
- useMessageBus();
-})
-
-Allows you to notify the user via toasts.
- const notify = useNotifyStore();
- notify.info('Just wanted to let you know');
- notify.warn('You might want to get concerned');
- notify.error('Be afraid');
- notify.success('Totally did it');
- notify.notify('info', 'Hello');
-
-You would then use the Notify Component in your layout.
-<client-only>
- <Notify />
-</client-only>
-
-A wrapper around the Nuxt composable $fetch()
that allows for streamlined fetches, but integrate’s with ving’s subsystems.
const response = useFetch('/api/user');
-
-Generally not something you’d need to use, but you will interact with it through the admin UI for the system wide alert, but it is used by the SystemWideAlert
component.
const swa = useSystemWideAlertStore();
-onMounted(async () => {
- swa.check();
-}
-
-Whenever there is an interaction with the API via the useRest()
composable it will update the throbber store. It is then used by the Throbber
component.
You may also wish to trigger it for other background actions that are happening so that your user knows something is going on at the moment. Maybe if you’re processing a file for export, or if you have some heavy calculations going on, or if you’re waiting on a web worker.
- const throbber = useThrobberStore();
- throbber.working();
- throbber.done();
-
-A client for interacting with server-side ving kinds through the Rest API.
-const users = useVingKind({
- listApi : '/api/user',
- createApi : '/api/user',
- query: { includeMeta: true, sortBy: 'username', sortOrder: 'asc' },
- newDefaults: { username: '', realName: '', email: '' },
-});
-await users.search();
-onBeforeRouteLeave(() => users.dispose());
-
-A client for interacting with server-side ving records through the Rest API.
-const id = route.params.id.toString();
-const user = useVingRecord<'User'>({
- id,
- fetchApi: '/api/user/' + id,
- createApi: '/api/user',
- query: { includeMeta: true, includeOptions: true },
- onUpdate() {
- notify.success('Updated user.');
- },
- async onDelete() {
- await navigateTo('/user/admin');
- },
-});
-await user.fetch()
-onBeforeRouteLeave(() => user.dispose());
-
-Basics
- -Subsystems
-Rest APIs
- - -Records are the functional implementation of a ving schema. They automatically generate a ton of functionality from making queries easier, to building web services, to setting up privileges and more.
-A ving record is technically 2 separate classes. The first is called a “Kind”, which is technically a group or list of records. In relational database terms, think of a kind as a table. The second type is the record, which is an instance of a kind, which in relational database terms is a row within a table. Both classes exist inside the record file. See #ving/record/records/User.mjs
as an example implementation of a record, or #ving/record/VingRecord.mjs
to see the base class that all ving records inherit from.
You can use the CLI to automatically generate a new record file for you from a Drizzle table. So if you’ve created a table called Foo
then you could create a new record file like this:
./ving.mjs record --new Foo
-
-That will generate the file #ving/record/records/Foo.mjs
. And in there you could add any custom functionality you may need. Or if you don’t need any custom functionality, then it may work just as it is.
Once you’re done adding functionality you can then generate a Rest API for it by invoing the CLI again like this:
-./ving.mjs record --rest Foo
-
-Those files will be placed in server/api/foo
and you can modify them as needed, but they should work without modification. And you can access them at http://localhost:3000/api/foo
.
And if you want to build a user interface for your services, you can generate that too by invoking the CLI once more.
-./ving.mjs record --web Foo
-
-Those files will be placed in pages/foo
and you can modify them as needed as well. And you can access them at http://localhost:3000/foo
.
The Kind is akin to a relational database table. To start with you need to get a reference to it:
-import {useKind} from '#ving/record/VingKind.mjs';
-const users = await useKind('User');
-
-You can create records many different ways. In all three methods you’ll pass in a list of props
which is an object containing the values (or columns) to set on the record.
Creates an in-memory copy of an existing record, but with a new id
. You’d later have to call insert()
(from the Record API) on the record to insert it into the database. This is essentially the same as passing the properties of an exsiting record to the mint()
method.
const copyOfRecord = Users.copy(existingRecord);
-
-Creates a new record in the database, but doesn’t validate the inputs according to the ving schema. That doesn’t mean it will get inserted. If you haven’t given enough information to pass the database table’s own schema, then it will fail. You are just bypassing the extra validation provided by ving’s schema. In general, you shouldn’t use this unless you know what you are doing.
-const record = await Users.create({username: 'Fred'});
-
-Creates a new record in memory, validates it against ving’s schema, and then inserts it into the database. This is almost always what you want.
-const record = await Users.createAndVerify({username: 'Fred'});
-
-Use this to write your own custom insert statement. Also see the insert()
method on the Record API.
const result = await Users.insert.values({username:'Fred'});
-
-Creates a record in memory. You’d later have to call insert()
(from the Record API) on the record to insert it into the database.
const record = Users.mint({username: 'Fred'});
-
-There are many different ways to find a record or list of records in ving.
-Instead of returning records like the rest of this list, describeList()
returns an array of objects that is suitable for using in web services. You can use it like this:
const list = await Users.describeList(params, where)
-
-{
- paging: {
- page: 1,
- nextPage: 2,
- previousPage: 1,
- itemsPerPage: 10,
- totalItems: 43,
- totalPages: 5
- },
- items: [
- { // same as the describe() method from a Record
- props : { id: 'xxx', ... }, // database properties
- meta : { displayName : 'Freddy', ... }, // calculated properties
- links: {
- self : {
- href : "/api/user/xxx",
- methods: ['GET','PUT','DELETE']
- },
- ...
- }, // urls for various web services
- options : { useAsDisplayName : [
- { label : 'Username', value : 'username' },
- ...
- ]}, // options for enumerated / boolean fields
- related: { }, // records with a parent/sibling relationship to this record
- },
- ...
- ]
-}
-
-1
and 100
that defaults to 10
and represents how many items should be included per page.1
and 1000000
that defaults to 1
and represents the current page number of the result set.prop
of the record and defaults to createdAt
.asc
but could also be desc
if you want the order of the records to be sorted in descending order.1
and 100000000000
that defaults to 100000000000
that limits the total number of items that can ever be paginated through.describe()
method in the Record API.findMany()
method below.Locates and returns a single record by it’s id
or undefined
if no record is found.
const record = await Users.find('xxx');
-
-Locates and returns a list of records by a drizzle where clause or an empty array if no records are found.
-const listOfFredRecords = await Users.findMany(like(Users.realName, 'Fred%'));
-
-Locates and returns a single record by a drizzle where clause or undefined
if no record is found.
const fredRecord = await Users.findOne(eq(Users.username, 'Fred'));
-
-Locates and returns a single record by it’s id
or throws a 404
error if no record is found.
const record = await Users.findOrDie('xxx');
-
-Write your own custom select function. Returns a drizzle result set, not a list of records.
-const results = await Users.select.where(like(Users.realName, 'Fred%'));
-
-Updating existing records.
-Update records already in the database without first selecting them by writing your own custom query.
-const results = await Users.update.set({admin: false}).where(like(Users.realName, 'Fred%'))
-
---Note that this where clause is raw. To use it safeley you should wrap the
-like(Users.realName, 'Fred%')
portion in thecalcWhere()
method below.
See also the update()
method in the Record API for updating a record you’ve already fetched.
Delete records by writing your own custom query.
-const results = await Users.delete.where(like(Users.realName, 'Fred%'));
-
---Note that this where clause is raw. To use it safeley you should wrap the
-like(Users.realName, 'Fred%')
portion in thecalcWhere()
method below.
See also the delete()
method in the Record API for deleting a record you’ve already fetched.
A safer version of delete
above as it uses calcWhere()
.
await Users.deleteMany(like(Users.realName, 'Fred%'));
-
-Adds propDefaults
(if any) into a where clause to limit the scope of affected records. As long as you’re using the built in queries you don’t need to use this method. But you might want to use it if you’re using create
, select
, update
, or delete
directly.
const results = await Users.delete.where(Users.calcWhere(like(Users.realName, 'Fred%')));
-
-Returns an integer representing how many records match a given where clause.
-const usersNamedFred = await Users.count(like(Users.realName, 'Fred%'));
-
-An array of objects containing a list of properties used in building relationships between this record and another. For example the User
record uses it like this to establish related APIKey
records:
apikeys.propDefaults.push({
- prop: 'userId',
- field: apikeys.table.userId,
- value: this.get('id')
-});
-
-It is then used with calcWhere()
to limit the scope of a where clause to related records. So for this example it would limite the list of APIKey
s to the ones related to the current User
.
The Record is akin to a relational database table row. You’ll get a record via the Kind API, perhaps like this:
-import {useKind} from '#ving/record/VingRecord.mjs';
-const users = await useKind('User');
-const record = users.findOrDie('xxx');
-
-Once you have a record you can use the following methods to manipulate it.
-Formats everything known about a record into an object that is easily serializable and sanitized for privileges. This is used by the Rest API to format a record for public consumption.
-const description = await record.describe(params)
-
-{
- props : { id: 'xxx', ... }, // database properties
- meta : { displayName : 'Freddy', ... }, // calculated properties
- links: { self : {
- href : "/api/user/xxx",
- methods: ['GET','PUT','DELETE']
- },
- ...
- }, // urls for various web services
- options : { useAsDisplayName : [
- { label : 'Username', value : 'username' },
- ...
- ]}, // options for enumerated / boolean fields
- related: { }, // records with a parent/sibling relationship to this record
- extra : {}, // an object that include literally anything special defined by the object
-}
-
-User
record or a Session
object can be used to determine what data should be included in the description based upon user privileges.true
will add list of enumerated options for props on this record.true
will include calculated properties.true
will include a list of API links.true
will ignore the privileges of the currentUser
passed in and include all private information.Returns the value of a single prop on this record using the name of the prop.
-const value = record.get('id');
-
-Returns an object containing key value pairs of all the props stored in the database for this record.
-const props = record.getAll();
-
-Refetches the data from the database for this record.
-await record.refresh();
-
-Calls testCreationProps()
, setPostedProps()
and insert()
in a single method call.
await record.createAndVerify({foo: 'bar', one: 1}, currentUserOrSession);
-
-Delete the current record from the database.
-await record.delete();
-
-Insert the current record into the database. Can only be called if it hasn’t already been inserted.
-await record.insert();
-
-Set the value of a prop on this record. Returns the value set or throws an error if there is a validation problem, which then throws an error. Updates the in memory prop, but doesn’t write it to the database; call update()
or insert()
for that.
user.set('admin', true);
-
-Set the values of multiple props on this record at the same time. Returns an object containing all the props unless there is a validation problem, which then throws an error. Updates the in memory props, but doesn’t write it to the database; call update()
or insert()
for that.
record.set({foo: 'bar', one: 1});
-
-Sets props on the current record from an untrusted source, and thus checks whether a specific user has the privileges to set the prop. Returns true
if everything sets properly, or throws an error if there are privilege problems. Updates the in memory props, but doesn’t write it to the database; call update()
or insert()
for that.
await record.setPostedProps({foo: 'bar', one: 1}, currentUserOrSession);
-
-Updates the current record in the database.
-Calls setPostedProps()
and update()
in a single method call.
await record.updateAndVerify({foo: 'bar', one: 1}, currentUserOrSession);
-
-Returns true
if the current user has edit rights on this record or throws a 403
error if not.
record.canEdit(currentUserOrSession);
-
-Returns true
if the current user or session is defined as the owner of this record, or returns false
if not.
if (record.isOwner(currentUserOrSession) {
- console.log('they own it!')
-}
-else {
- console.log('they do NOT own it!')
-}
-
-Add a warning to the list of warnings
so that the user can be notified in the UI.
record.addWarning({
- code : 418,
- message : "I'm a little teapot."
- });
-
-Returns true
if this record has been inserted into the database, or false
if not.
Returns a parent relationship record by name.
-const user = apikey.parent('user');
-
-Returns a list of enumeration options for this record.
-const options = await user.propOptions(params, false);
-
-{
- useAsDisplayName : [
- { label : 'Username', value : 'username' },
- ...
- ],
- ...
-}
-
-describe()
method.Tests the properties trying to be set on a new object, and if all the required values are present and valid then it returns true
, otherwise it throws a 441
error.
await record.testCreationProps({foo:'bar'});
-
-An array of warnings for this record that have been added by addWarnings()
since this record was instanciated.
[
- {
- code : 418,
- message : "I'm a little teapot."
- },
- ...
-]
-
-Basics
- -Subsystems
-Rest APIs
- - -You’ll find the schemas in #ving/schema/schemas
. A schema looks like this:
{
- kind: 'User',
- tableName: 'users',
- owner: ['$id', 'admin'],
- props: [
- ...baseSchemaProps,
- {
- type: "string",
- name: "username",
- required: true,
- unique: true,
- length: 60,
- default: '',
- db: (prop) => dbString(prop),
- zod: (prop) => zodString(prop),
- view: [],
- edit: ['owner'],
- },
- ],
-}
-
-Use the CLI to generate a new schema skeleton to work from.
-./ving.mjs schema --new=Foo
-
-A string that represents the unique name of the schema and everything generated from it.
-A string that is name of the MySQL database table it will be stored in.
-An array containing the method for determining who owns this object. Owners can be assigned special rights on props
.
The owner can be any field that contains a User ID, so in the case of the User
schema, it can use its own id by specifying $id
, but in another table it might be $userId
.
It can also contain any number of roles. By default there are 3 roles: admin
, developer
, verifiedEmail
, but you could add more. Roles can be defined inside the User
schema (#ving/schema/schemas/User.mjs
). They have to be added as a boolean prop type, and then also added to the RoleOptions
at the bottom of the file.
It can also defer to a parent object. So let’s say you had a record called Invoice and another called LineItem. Each LineItem would have a parent relation to the Invoice called invoice
. So you could then use ^invoice
(notice the carat) to indicate that you’d like to ask the Invoice if the User owns it, and if the answer is yes, then the LineItem will be considered to also be owned by that user. The carat means “look for a parent relation in the schema” and whatever comes after the carat is the name of that relation.
All schemas should have the base props of id
, createdAt
, and updatedAt
by using ...baseSchemaProps
. After that it’s up to you to add your own props to the list. There are many different types of props for different field types.
Props all have the fields type
, name
, required
, default
, db
, zod
, view
, and edit
, but can have more or less fields from there.
The view
and edit
props are arrays that can:
public
, if everyone should be able to view or edit that propadmin
)owner
, so that whoever was defined as the owner in the attributes of the schema can view or edit that propIf a user can edit
a prop it can automatically view
a prop.
{
- type: "boolean",
- name: 'admin',
- required: true,
- default: false,
- db: (prop) => dbBoolean(prop),
- enums: [false, true],
- enumLabels: ['Not Admin', 'Admin'],
- view: ['owner'],
- edit: ['admin'],
-},
-
-{
- type: "date",
- name: "startAt",
- required: true,
- autoUpdate: true,
- default: () => new Date(),
- db: (prop) => dbDateTime(prop),
- view: ['public'],
- edit: [],
-},
-
-{
- type: "enum",
- name: 'useAsDisplayName',
- required: true,
- length: 20,
- default: 'username',
- db: (prop) => dbEnum(prop),
- enums: ['username', 'email', 'realName'],
- enumLabels: ['Username', 'Email Address', 'Real Name'],
- view: [],
- edit: ['owner'],
-},
-
-These are used to add a parent relationship.
-{
- type: "id",
- name: 'userId',
- required: true,
- length: 36,
- db: (prop) => dbRelation(prop),
- relation: {
- type: 'parent',
- name: 'user',
- kind: 'User',
- },
- default: undefined,
- view: ['public'],
- edit: ['owner'],
-},
-
-{
- type: "string",
- name: "email",
- required: true,
- unique: true,
- length: 256,
- default: '',
- db: (prop) => dbString(prop),
- zod: (prop) => zodString(prop).email(),
- view: [],
- edit: ['owner'],
-},
-
-
-These are used to add a child relationship. They are virtual because they make no modification to the database table they represent.
-{
- type: "virtual",
- name: 'userId',
- required: false,
- view: ['public'],
- edit: [],
- relation: {
- type: 'child',
- name: 'apikeys',
- kind: 'APIKey',
- },
-},
-
-Now that you’ve created or updated your schema, you can generate Drizzle tables from with this command:
-./ving.mjs schema --tables
-
---Note that you shouldn’t ever need to modify these table files directly. If you need to change them, update the ving schema and run the above command again.
-