Browse Source

Add example project

Include example project and requests.
master
TheoryOfNekomata 7 months ago
parent
commit
342a2e6e0c
23 changed files with 451 additions and 132 deletions
  1. +0
    -16
      packages/core/examples/basic/data-source.ts
  2. +0
    -29
      packages/core/examples/basic/serializers.ts
  3. +0
    -75
      packages/core/examples/basic/server.ts
  4. +0
    -1
      packages/core/examples/basic/users.jsonl
  5. +13
    -9
      packages/core/src/backend/servers/http/core.ts
  6. +1
    -1
      packages/core/src/common/delta/core.ts
  7. +1
    -0
      packages/core/test/utils.ts
  8. +18
    -1
      packages/data-sources/file-jsonl/package.json
  9. +108
    -0
      packages/examples/cms-web-api/.gitignore
  10. +18
    -0
      packages/examples/cms-web-api/bruno/Create Post.bru
  11. +11
    -0
      packages/examples/cms-web-api/bruno/Delete Post.bru
  12. +11
    -0
      packages/examples/cms-web-api/bruno/Get Single Post.bru
  13. +25
    -0
      packages/examples/cms-web-api/bruno/Modify Post (Delta).bru
  14. +23
    -0
      packages/examples/cms-web-api/bruno/Modify Post (Merge).bru
  15. +11
    -0
      packages/examples/cms-web-api/bruno/Query Posts.bru
  16. +19
    -0
      packages/examples/cms-web-api/bruno/Replace Post.bru
  17. +13
    -0
      packages/examples/cms-web-api/bruno/bruno.json
  18. +50
    -0
      packages/examples/cms-web-api/package.json
  19. +3
    -0
      packages/examples/cms-web-api/pridepack.json
  20. +67
    -0
      packages/examples/cms-web-api/src/index.ts
  21. +8
    -0
      packages/examples/cms-web-api/test/index.test.ts
  22. +23
    -0
      packages/examples/cms-web-api/tsconfig.json
  23. +28
    -0
      pnpm-lock.yaml

+ 0
- 16
packages/core/examples/basic/data-source.ts View File

@@ -1,16 +0,0 @@
import {DataSource} from '../../src/backend/data-source';

export const autoIncrement = async (dataSource: DataSource) => {
const data = await dataSource.getMultiple() as Record<string, string>[];

const highestId = data.reduce<number>(
(highestId, d) => (Number(d.id) > highestId ? Number(d.id) : highestId),
-Infinity
);

if (Number.isFinite(highestId)) {
return (highestId + 1);
}

return 1;
};

+ 0
- 29
packages/core/examples/basic/serializers.ts View File

@@ -1,29 +0,0 @@
export const TEXT_SERIALIZER_PAIR = {
name: 'text/plain',
serialize(obj: unknown, level = 0): string {
if (Array.isArray(obj)) {
return obj.map((o) => this.serialize(o)).join('\n\n');
}

if (typeof obj === 'object') {
if (obj !== null) {
return Object.entries(obj)
.map(([key, value]) => `${Array(level * 2).fill(' ').join('')}${key}: ${this.serialize(value, level + 1)}`)
.join('\n');
}

return '';
}

if (typeof obj === 'number' && Number.isFinite(obj)) {
return obj.toString();
}

if (typeof obj === 'string') {
return obj;
}

return '';
},
deserialize: <T>(str: string) => str as T
};

+ 0
- 75
packages/core/examples/basic/server.ts View File

@@ -1,75 +0,0 @@
import {
application,
resource,
validation as v,
} from '../../src/common';
import { dataSources } from '../../src/backend';
import {TEXT_SERIALIZER_PAIR} from './serializers';
import {autoIncrement} from './data-source';

const Piano = resource(v.object(
{
brand: v.string()
},
v.never()
))
.name('Piano')
.route('pianos')
.canFetchItem()
.canFetchCollection()
.canCreate()
.canEmplace()
.canPatch()
.canDelete()
.id('id', {
generationStrategy: autoIncrement,
serialize: (id) => id?.toString() ?? '0',
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0,
schema: v.number(),
});

const User = resource(v.object(
{
firstName: v.string(),
middleName: v.string(),
lastName: v.string(),
bio: v.string(),
birthday: v.datelike()
},
v.never()
))
.name('User')
.route('users')
.fullText('bio')
.id('id' as const, {
generationStrategy: autoIncrement,
serialize: (id) => id?.toString() ?? '0',
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0,
schema: v.number(),
});

const app = application({
name: 'piano-service',
})
.mediaType(TEXT_SERIALIZER_PAIR)
.resource(Piano)
.resource(User);

const backend = app.createBackend({
dataSource: new dataSources.jsonlFile.DataSource('examples/basic'),
});

const server = backend.createHttpServer({
basePath: '/api'
});

server.listen(3000);

setTimeout(() => {
// Allow user operations after 5 seconds from startup
User
.canFetchItem()
.canFetchCollection()
.canCreate()
.canPatch();
}, 5000);

+ 0
- 1
packages/core/examples/basic/users.jsonl View File

@@ -1 +0,0 @@
{"firstName":"John","middleName":"Smith","lastName":"Doe","bio":"This is my updated bio!","birthday":"1986-04-20T00:00:00.000Z","id":1}

+ 13
- 9
packages/core/src/backend/servers/http/core.ts View File

@@ -261,7 +261,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
const theBodyStr = encodingPair.decode(theBodyBuffer);
const theBody = deserializerPair.deserialize(theBodyStr);
try {
// for validation, I wonder why an empty object is returned for PATCH when both methods are enabled
req.body = await v.parseAsync(bodySchema, theBody, {abortEarly: false, abortPipeEarly: false});
req.body = theBody;
} catch (errRaw) {
const err = errRaw as v.ValiError;
// todo use error message key for each method
@@ -432,11 +434,12 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr

const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => {
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
const language = plainReq.cn.language ?? plainReq.backend.cn.language;
const mediaType = plainReq.cn.mediaType ?? plainReq.backend.cn.mediaType;
const charset = plainReq.cn.charset ?? plainReq.backend.cn.charset;

if (typeof plainReq.resource !== 'undefined') {
const resourceReq = plainReq as ResourceRequestContext;
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
const mediaType = resourceReq.cn.mediaType ?? resourceReq.backend.cn.mediaType;
const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset;

// TODO custom middlewares
const effectiveMiddlewares = (
@@ -451,6 +454,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
try {
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this
} catch (processRequestErrRaw) {
// TODO add error handlers
handleMiddlewareError(processRequestErrRaw as Error)(resourceReq, res);
return;
}
@@ -528,28 +532,28 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
}

if (middlewares.length > 0) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g,
res.statusMessage = language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, {
Allow: middlewares.map((m) => m.method).join(', '),
'Content-Language': resourceReq.backend.cn.language.name,
'Content-Language': language.name,
});
res.end();
return;
}
res.statusMessage = resourceReq.backend.cn.language.statusMessages.urlNotFound.replace(/\$RESOURCE/g,
res.statusMessage = language.statusMessages.urlNotFound.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, {
'Content-Language': resourceReq.backend.cn.language.name,
'Content-Language': language.name,
});
res.end();
return;
}

res.statusMessage = reqRaw.backend.cn.language.statusMessages.notImplemented.replace(/\$RESOURCE/g,
res.statusMessage = language.statusMessages.notImplemented.replace(/\$RESOURCE/g,
reqRaw.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED, {
'Content-Language': reqRaw.backend.cn.language.name,
'Content-Language': language.name,
});
res.end();
};


+ 1
- 1
packages/core/src/common/delta/core.ts View File

@@ -176,7 +176,7 @@ export const applyDelta = async <T extends v.BaseSchema = v.BaseSchema>(
}

mutateObject(mutablePreviousObject, deltaItem, pathSchema);
if (!v.is(resourceObjectSchema, mutablePreviousObject)) {
if (!v.is(resourceObjectSchema, mutablePreviousObject, { skipPipe: true })) {
throw new InvalidOperationError();
}



+ 1
- 0
packages/core/test/utils.ts View File

@@ -27,6 +27,7 @@ export const createTestClient = (options: Omit<RequestOptions, 'method' | 'path'
...etcAdditionalHeaders
} = additionalHeaders;

// odd that request() uses OutgoingHttpHeaders instead of IncomingHttpHeaders...
const headers: OutgoingHttpHeaders = {
...(options.headers ?? {}),
...etcAdditionalHeaders,


+ 18
- 1
packages/data-sources/file-jsonl/package.json View File

@@ -1,5 +1,5 @@
{
"name": "data-source-file-jsonl",
"name": "@modal-sh/yasumi-data-source-file-jsonl",
"version": "0.0.0",
"files": [
"dist",
@@ -45,5 +45,22 @@
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "public"
},
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/production/index.js",
"module": "./dist/esm/production/index.js",
"exports": {
".": {
"development": {
"require": "./dist/cjs/development/index.js",
"import": "./dist/esm/development/index.js"
},
"require": "./dist/cjs/production/index.js",
"import": "./dist/esm/production/index.js",
"types": "./dist/types/index.d.ts"
}
},
"typesVersions": {
"*": {}
}
}

+ 108
- 0
packages/examples/cms-web-api/.gitignore View File

@@ -0,0 +1,108 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.production
.env.development

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.npmrc
*.jsonl

+ 18
- 0
packages/examples/cms-web-api/bruno/Create Post.bru View File

@@ -0,0 +1,18 @@
meta {
name: Create Post
type: http
seq: 2
}

post {
url: http://localhost:6969/api/posts
body: json
auth: none
}

body:json {
{
"title": "New Post",
"content": "Hello there"
}
}

+ 11
- 0
packages/examples/cms-web-api/bruno/Delete Post.bru View File

@@ -0,0 +1,11 @@
meta {
name: Delete Post
type: http
seq: 8
}

delete {
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2
body: none
auth: none
}

+ 11
- 0
packages/examples/cms-web-api/bruno/Get Single Post.bru View File

@@ -0,0 +1,11 @@
meta {
name: Get Single Post
type: http
seq: 3
}

get {
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2
body: none
auth: none
}

+ 25
- 0
packages/examples/cms-web-api/bruno/Modify Post (Delta).bru View File

@@ -0,0 +1,25 @@
meta {
name: Modify Post (Delta)
type: http
seq: 5
}

patch {
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2
body: json
auth: none
}

headers {
Content-Type: application/json-patch+json
}

body:json {
[
{
"op": "replace",
"path": "content",
"value": "I changed the value via delta."
}
]
}

+ 23
- 0
packages/examples/cms-web-api/bruno/Modify Post (Merge).bru View File

@@ -0,0 +1,23 @@
meta {
name: Modify Post (Merge)
type: http
seq: 4
}

patch {
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2
body: json
auth: none
}

headers {
Content-Type: application/merge-patch+json
}

body:json {
{
"title": "Modified Post",
"content": "I changed the content via merge."
}
}

+ 11
- 0
packages/examples/cms-web-api/bruno/Query Posts.bru View File

@@ -0,0 +1,11 @@
meta {
name: Query Posts
type: http
seq: 1
}

get {
url: http://localhost:6969/api/posts
body: none
auth: none
}

+ 19
- 0
packages/examples/cms-web-api/bruno/Replace Post.bru View File

@@ -0,0 +1,19 @@
meta {
name: Replace Post
type: http
seq: 7
}

put {
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2
body: json
auth: none
}

body:json {
{
"title": "Replaced Post",
"content": "The old content is gone.",
"id": "5fac64d6-d261-42bb-a67b-bc7e1955a7e2"
}
}

+ 13
- 0
packages/examples/cms-web-api/bruno/bruno.json View File

@@ -0,0 +1,13 @@
{
"version": "1",
"name": "cms-web-api",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"presets": {
"requestType": "http",
"requestUrl": "http://localhost:6969/api/"
}
}

+ 50
- 0
packages/examples/cms-web-api/package.json View File

@@ -0,0 +1,50 @@
{
"name": "@modal-sh/yasumi-example-cms-web-api",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=16"
},
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^20.11.0",
"pridepack": "2.6.0",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vitest": "^1.2.0"
},
"dependencies": {
"@modal-sh/yasumi": "*",
"@modal-sh/yasumi-data-source-file-jsonl": "*",
"tsx": "^4.7.1"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "tsx src/index.ts",
"dev": "tsx watch src/index.ts",
"test": "vitest"
},
"private": true,
"description": "CMS example service for Yasumi",
"repository": {
"url": "",
"type": "git"
},
"homepage": "",
"bugs": {
"url": ""
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "restricted"
}
}

+ 3
- 0
packages/examples/cms-web-api/pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 67
- 0
packages/examples/cms-web-api/src/index.ts View File

@@ -0,0 +1,67 @@
import { application, resource, validation as v } from '@modal-sh/yasumi';
import { randomUUID } from 'crypto';
import {JsonLinesDataSource} from '@modal-sh/yasumi-data-source-file-jsonl';

const User = resource(
v.object({
email: v.string([v.email()]),
password: v.string(),
createdAt: v.datelike(),
updatedAt: v.datelike(),
})
)
.name('User')
.route('users')
.id('id', {
generationStrategy: () => Promise.resolve(randomUUID()),
schema: v.string(),
serialize: (v: unknown) => v?.toString() ?? '',
deserialize: (v: string) => v,
})
.canFetchItem()
.canFetchCollection()
.canCreate()
.canEmplace()
.canPatch()
.canDelete();

const now = () => new Date();

const Post = resource(
v.object({
title: v.string(),
content: v.string(),
createdAt: v.optional(v.datelike(), now),
updatedAt: v.optional(v.datelike(), now),
})
)
.name('Post')
.route('posts')
.id('id', {
generationStrategy: () => Promise.resolve(randomUUID()),
schema: v.string(),
serialize: (v: unknown) => v?.toString() ?? '',
deserialize: (v: string) => v,
})
.canFetchItem()
.canFetchCollection()
.canCreate()
.canEmplace()
.canPatch()
.canDelete();

const app = application({
name: 'cms'
})
.resource(User)
.resource(Post);

const backend = app.createBackend({
dataSource: new JsonLinesDataSource(),
});

const server = backend.createHttpServer({
basePath: '/api',
});

server.listen(6969);

+ 8
- 0
packages/examples/cms-web-api/test/index.test.ts View File

@@ -0,0 +1,8 @@
import { describe, it, expect } from 'vitest';
import add from '../src';

describe('blah', () => {
it('works', () => {
expect(add(1, 1)).toEqual(2);
});
});

+ 23
- 0
packages/examples/cms-web-api/tsconfig.json View File

@@ -0,0 +1,23 @@
{
"exclude": ["node_modules"],
"include": ["src", "types"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"useDefineForClassFields": false,
"declarationMap": true
}
}

+ 28
- 0
pnpm-lock.yaml View File

@@ -59,6 +59,34 @@ importers:
specifier: ^1.2.0
version: 1.5.0(@types/node@20.12.7)

packages/examples/cms-web-api:
dependencies:
'@modal-sh/yasumi':
specifier: '*'
version: link:../../core
'@modal-sh/yasumi-data-source-file-jsonl':
specifier: '*'
version: link:../../data-sources/file-jsonl
tsx:
specifier: ^4.7.1
version: 4.7.2
devDependencies:
'@types/node':
specifier: ^20.11.0
version: 20.12.7
pridepack:
specifier: 2.6.0
version: 2.6.0(tslib@2.6.2)(typescript@5.4.5)
tslib:
specifier: ^2.6.2
version: 2.6.2
typescript:
specifier: ^5.3.3
version: 5.4.5
vitest:
specifier: ^1.2.0
version: 1.5.0(@types/node@20.12.7)

packages:

/@esbuild/aix-ppc64@0.19.12:


Loading…
Cancel
Save