I'm creating a REST API using AWS CDK version 1.22 and I would like to document my API using CDK as well, but I do not see any documentation generated for my API after deployment.
I've dived into aws docs, cdk example, cdk reference but I could find concrete examples that help me understand how to do it.
Here is my code:
const app = new App();
const api = new APIStack(app, 'APIStack', { env }); // basic api gateway
// API Resources
const resourceProps: APIResourceProps = {
gateway: api.gateway,
}
// dummy endpoint with some HTTP methods
const siteResource = new APISiteStack(app, 'APISiteStack', {
env,
...resourceProps
});
const siteResourceDocs = new APISiteDocs(app, 'APISiteDocs', {
env,
...resourceProps,
});
// APISiteDocs is defined as follow:
class APISiteDocs extends Stack {
constructor(scope: Construct, id: string, props: APIResourceProps) {
super(scope, id, props);
new CfnDocumentationVersion(this, 'apiDocsVersion', {
restApiId: props.gateway.restApiId,
documentationVersion: config.app.name(`API-${config.gateway.api.version}`),
description: 'Spare-It API Documentation',
});
new CfnDocumentationPart(this, 'siteDocs', {
restApiId: props.gateway.restApiId,
location: {
type: 'RESOURCE',
method: '*',
path: APISiteStack.apiBasePath,
statusCode: '405',
},
properties: `
{
"status": "error",
"code": 405,
"message": "Method Not Allowed"
}
`,
});
}
}
Any help/hint is appreciated, Thanks.
I have tested with CDK 1.31 and it is possible to use the CDK's default deployment option and also add a document version to the stage. I have used the deployOptions.documentVersion in rest api definition to set the version identifier of the API documentation:
import * as cdk from '#aws-cdk/core';
import * as apigateway from "#aws-cdk/aws-apigateway";
import {CfnDocumentationPart, CfnDocumentationVersion} from "#aws-cdk/aws-apigateway";
export class CdkSftpStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const documentVersion = "v1";
// create the API
const api = new apigateway.RestApi(this, 'books-api', {
deploy: true,
deployOptions: {
documentationVersion: documentVersion
}
});
// create GET method on /books resource
const books = api.root.addResource('books');
books.addMethod('GET');
// // create documentation for GET method
new CfnDocumentationPart(this, 'doc-part1', {
location: {
type: 'METHOD',
method: 'GET',
path: books.path
},
properties: JSON.stringify({
"status": "successful",
"code": 200,
"message": "Get method was succcessful"
}),
restApiId: api.restApiId
});
new CfnDocumentationVersion(this, 'docVersion1', {
documentationVersion: documentVersion,
restApiId: api.restApiId,
description: 'this is a test of documentation'
});
}
}
From what I can gather, if you use the CDK's default deployment options which create stage and deployment on your behalf, it won't be possible to append the stage with a documentation version set.
Instead, the solution would be to set the RESTAPI's option object to deploy:false and define the stage and deployment manually.
stack.ts code
import * as cdk from '#aws-cdk/core';
import * as apigateway from '#aws-cdk/aws-apigateway';
import { Stage, Deployment, CfnDocumentationPart, CfnDocumentationVersion, CfnDeployment } from '#aws-cdk/aws-apigateway';
export class StackoverflowHowToDocumentRestApiUsingAwsCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// create the API, need to not rely on CFN's automatic deployment because we need to
// make our own deployment to set the documentation we create
const api = new apigateway.RestApi(this, 'books-api',{
deploy: false
});
// create GET method on /books resource
const books = api.root.addResource('books');
books.addMethod('GET');
// // create documentation for GET method
const docpart = new CfnDocumentationPart(this, 'doc-part1', {
location: {
type: 'METHOD',
method: 'GET',
path: books.path
},
properties: JSON.stringify({
"status": "successful",
"code": 200,
"message": "Get method was succcessful"
}),
restApiId: api.restApiId
});
const doc = new CfnDocumentationVersion(this, 'docVersion1', {
documentationVersion: 'version1',
restApiId: api.restApiId,
description: 'this is a test of documentation'
});
// not sure if this is necessary but it made sense to me
doc.addDependsOn(docpart);
const deployment = api.latestDeployment ? api.latestDeployment: new Deployment(this,'newDeployment',{
api: api,
description: 'new deployment, API Gateway did not make one'
});
// create stage of api with documentation version
const stage = new Stage(this, 'books-api-stage1', {
deployment: deployment,
documentationVersion: doc.documentationVersion,
stageName: 'somethingOtherThanProd'
});
}
}
OUTPUT:
Created a feature request for this option here.
I had the same exact problem. The CfnDocumentationVersion call has to occur after you create all of your CfnDocumentationPart. Using your code as an example, it should look something like this:
class APISiteDocs extends Stack {
constructor(scope: Construct, id: string, props: APIResourceProps) {
super(scope, id, props);
new CfnDocumentationPart(this, 'siteDocs', {
restApiId: props.gateway.restApiId,
location: {
type: 'RESOURCE',
method: '*',
path: APISiteStack.apiBasePath,
statusCode: '405',
},
properties: JSON.stringify({
"status": "error",
"code": 405,
"message": "Method Not Allowed"
}),
});
new CfnDocumentationVersion(this, 'apiDocsVersion', {
restApiId: props.gateway.restApiId,
documentationVersion: config.app.name(`API-${config.gateway.api.version}`),
description: 'Spare-It API Documentation',
});
}
}
Related
I am setting up the ability to disconnect the session from my wallet app in React-Native. My code looks like this which matches the docs except that I am importing "getSdkError" from WalletConnectUtils:
import { Core } from "#walletconnect/core";
import { Web3Wallet } from "#walletconnect/web3wallet";
import "#walletconnect/react-native-compat";
import * as WalletConnectUtils from '#walletconnect/utils';
import { CONNECT_WALLET_PROJECT_ID } from '#env';
const core = new Core({
projectId: CONNECT_WALLET_PROJECT_ID
});
const metadata = {
name: '<company name>',
description: 'A Wallet Application',
url: "#",
icons: []
};
const web3wallet = await Web3Wallet.init({
core,
metadata: metadata
});
const disconnect = await web3wallet.disconnectSession({
topic: topic,
reason: WalletConnectUtils.getSdkError("USER_DISCONNECTED"),
});
To completely match the docs I have also tried this:
const disconnect = await web3wallet.disconnectSession({
topic,
reason: WalletConnectUtils.getSdkError("USER_DISCONNECTED"),
});
When I run this though I get the error:
Object {
"context": "core",
}, Object {
"context": "core/pairing",
}, Object {
"code": 10001,
"message": "Unsupported wc_ method. wc_pairingDelete",
}
I checked to see what WalletConnectUtils.getSdkError("USER_DISCONNECTED") returns and got this:
Object {
"code": 6000,
"message": "User disconnected.",
}
I've been using https://react-app.walletconnect.com/ to test and it doesn't seem to disconnect on that site when I run this so I want to make sure I am disconnecting successfully.
i want to seed data onConnect, but i have access denied, using this query :
{
keystone: keystone {
adminMeta {
lists {
key
description
label
singular
plural
path
fields {
path
}
}
}
}
i have this error even iam using sudo, context.sudo().graphql.raw :
[
Error: Access denied
at /Users/sidalitemkit/work/web/yet/wirxe/wirxe-app/node_modules/#keystone-next/admin-ui/system/dist/admin-ui.cjs.dev.js:552:19
at processTicksAndRejections (node:internal/process/task_queues:94:5)
at async Promise.all (index 0)
at async Promise.all (index 0) {
locations: [ [Object] ],
path: [ 'keystone', 'adminMeta' ]
}
]
here my config :
export default auth.withAuth(
config({
db: {
adapter: 'prisma_postgresql',
url:
'postgres://admin:aj093bf7l6jdx5hm#wirxe-app-database-do-user-9126376-0.b.db.ondigitalocean.com:25061/wirxepool?schema=public&pgbouncer=true&sslmode=require',
onConnect: initialiseData,
},
ui: {
isAccessAllowed: (context) => !!context.session?.data,
},
lists,
session: withItemData(
statelessSessions({
maxAge: sessionMaxAge,
secret: sessionSecret,
}),
{ User: 'email' },
),
}),
);
i figured out that when i do :
isAccessAllowed: (context) => true
it's working
any advice here
context.sudo() disabled access control. there could be some issue with your query. isAccessAllowed: (context) => true is related to admin-ui and not to the backend implementation of graphql. This could be a bug please open a bug in the repo. They whould be able to fix it quickly.
I do not see sample initialiseData to try myself. Also the graphql is designed as such if you try to access some non existing item then it may give you access denied error even though there is not access control (all access set to true).
There is also another api which is easier in creating the initial items. You should use new list api, available as context.sudo().lists.<ListName>.createOne or createMany like this
const user = await context.sudo().lists.User.createOne({
data: {
name: 'Alice',
posts: { create: [{ title: 'My first post' }] },
},
query: 'id name posts { id title }',
});
or
const users = await context.lists.User.createOne({
data: [
{
data: {
name: 'Alice',
posts: [{ create: { title: 'Alices first post' } }],
},
},
{
data: {
name: 'Bob',
posts: [{ create: { title: 'Bobs first post' } }],
},
},
],
query: 'id name posts { id title }',
});
for more details see List Items API and Database Items API in their preview documentation.
You can find a working example in keystonejs repository (blog)
You have to await and pass context to the initialiseData() method. The onConnect hook already provides this context for you
also, you can look for an argument like '--seed-data' so it's only run once
and run the code as:
keystone --seed-data
export default auth.withAuth(
config({
db: {
adapter: 'prisma_postgresql',
url:
'postgres://admin:aj093bf7l6jdx5hm#wirxe-app-database-do-user-9126376-0.b.db.ondigitalocean.com:25061/wirxepool?schema=public&pgbouncer=true&sslmode=require',
async onConnect(context) {
if (process.argv.includes('--seed-data')) {
await initialiseData(context);
}
},
},
ui: {
isAccessAllowed: (context) => !!context.session?.data,
},
lists,
session: withItemData(
statelessSessions({
maxAge: sessionMaxAge,
secret: sessionSecret,
}),
{ User: 'email' },
),
}),
);
I created a Vue component which exports an async function. This component acts as a wrapper for calling my API. It's based on axios with a caching component that relies on localforage for some short lived persistence.
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'
export default {
async cache() {
// Register the custom `memoryDriver` to `localforage`
await localforage.defineDriver(memoryDriver)
// Create `localforage` instance
const store = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'tgi-cache'
})
// Create `axios` instance with pre-configured `axios-cache-adapter` using a `localforage` store
return setup({
// `axios` options
baseURL: 'https://my.api',
cache: {
maxAge: 2 * 60 * 1000, // set cache time to 2 minutes
exclude: { query: false }, // cache requests with query parameters
store // pass `localforage` store to `axios-cache-adapter`
}
})
}
}
Here is how I am importing and using this component in my views:
import api from '#/components/Api.vue'
export default {
data() {
return {
userId: this.$route.params.id,
userData: ''
}
},
methods: {
loadClient(userId) {
const thisIns = this;
api.cache().then(async (api) => {
const response = await api.get('/client/find?id='+userId)
thisIns.userData = response.data.data[0]
}).catch(function (error) {
console.log(error)
})
},
},
created() {
this.loadClient(this.userId)
},
}
I can import this component and everything appears to work. I get data back from my API. However, immediately after every call, I get an error:
TypeError: Cannot read property 'cache' of undefined
Which references this line:
api.cache().then(async (api) => {
I am unable to understand why this is happening, or what it means. The error itself indicates that the component I am importing is undefined, though that's clearly not the case; if it were, the API call would ultimately fail I would suspect. Instead, I am lead to believe that perhaps I am not constructing/exporting my async cache() function properly.
Upon further review, I don't actually understand why the author has implemented it the way he has. Why would you want to create an instance of localForage every single time you make an API call?
I've opted not to use a component and to only instantiate an instance of localForage once.
main.js
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'
// Register the custom `memoryDriver` to `localforage`
localforage.defineDriver(memoryDriver)
// Create `localforage` instance
const localforageStore = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'my-cache'
})
Vue.prototype.$http = setup({
baseURL: 'https://my.api',
cache: {
maxAge: 2 * 60 * 1000, // set cache time to 2 minutes
exclude: { query: false }, // cache requests with query parameters
localforageStore // pass `localforage` store to `axios-cache-adapter`
}
})
the view
export default {
data() {
return {
userId: this.$route.params.id,
userData: ''
}
},
methods: {
loadClient(userId) {
const thisIns = this;
thisIns.$http.get('/client/find?id='+userId)
.then(async (response) => {
thisIns.userData = response.data.data[0]
})
.catch(function (error) {
console.log(error)
})
},
},
created() {
this.loadClient(this.userId)
},
}
I have meet this situation for my requirements:
step 1. save data to local db which in the mobile phone (realm)
step 2. upload the local data to the server, and the server will return the data ids if success
step 3. delete the records in local db by the returned ids which get by step2
Realm.open({schema:[MySchame],encryptionKey:getRealmKey()})
.then(realm =>{
realm.write(() => {
// 1. get all step data from db
let objetcs = realm.objects('MySchema');
// 2. upload obtained data to server
if(objetcs.length > 0){
let recordArr = [];
for (let o of steps){
recordArr.push(o.get());
}
uploadDataToServer(recordArr,(res)=>{
//3. filter the uploaded steps and delete them
let uploadedSteps = realm.objects('MySchema').filtered('id=$0',res.id);
if(uploadedSteps.length > 0){
realm.delete(uploadedSteps);
}
});
}
});
realm.close();
})
.catch(error =>{
console.log(error);
});
but this is not works as expected, it seems DB is closed too early than networks success callback.
Thanks for any ideas.
Finally ,I use realm like this:
let realm = new Realm({schema:[JOB_SCHEMA.jobTrack],encryptionKey:getRealmKey()});
let objects = realm.objects('JobTrack');
realm.beginTransaction();
realm.delete(objects);
realm.commitTransaction();
realm.close();
First create a service like one below
import repository from "./realmConfig";
let CatalogsService = {
findAll: function () {
return repository.objects("CatalogsModel");
},
save: function (catalogs) {
repository.write(() => {
repository.create("CatalogsModel", catalogs);
});
},
delete: function () {
repository.write(() => {
let all = repository.objects("CatalogsModel");
repository.delete(all);
});
},
update: function (catalogs, callback) {
if (!callback) return;
repository.write(() => {
callback();
catalogs.updatedAt = new Date();
});
}
};
module.exports = CatalogsService;
where my realmConfig file is as
import Realm from "realm";
class CatalogsModel extends Realm.Object { }
CatalogsModel.schema = {
name: "CatalogsModel",
primaryKey: "id",
properties: {
id: "string",
name: "string",
url: "string",
status: "int"
}
};
class OffersModel extends Realm.Object { }
OffersModel.schema = {
name: "OffersModel",
primaryKey: "id",
properties: {
id: "string",
name: "string",
url: "string",
status: "int",
machineId: "string",
machineName: "string"
}
};
export default new Realm({
schema: [CatalogsModel, OffersModel],
schemaVersion: 1,
deleteRealmIfMigrationNeeded: true
});
Now import Service.js where you are calling async server call and do your job. For reference see below code
import CatalogService from './path/to/CatalogService .js'
//get objects
var catalogs = CatalogsService.findAll();
// fire async function , I prefer axios for network calls
Axios.post("SERVER_URL", {
data: catalogs
})
.then(function (response) {
if (response.success)
CatalogsService.delete()
}
I assume you can easily modify findAll() and delete() method as per your need
I'm using the ember-cli-simple-auth addon with Torii for my authentication flow.
So far I've managed to get authentication working using both a custom Torii Provider combined with a custom Simple Auth Authenticator.
I'd now like to use a custom Simple Auth Authorizer to inject the access token into requests.
Following the documentation https://github.com/simplabs/ember-simple-auth#implementing-a-custom-authorizer I've added a custom authorizer & initializer
authorizers/myservice.js
import Base from 'simple-auth/authorizers/base';
import Ember from 'ember';
export default Base.extend({
/**
#method authorize
#param {jqXHR} jqXHR The XHR request to authorize (see http://api.jquery.com/jQuery.ajax/#jqXHR)
#param {Object} requestOptions The options as provided to the `$.ajax` method (see http://api.jquery.com/jQuery.ajaxPrefilter/)
*/
authorize: function(jqXHR) {
var accessToken = this.get('session.content.token');
if (this.get('session.isAuthenticated') && !Ember.isEmpty(accessToken)) {
jqXHR.setRequestHeader('Authorization', 'Bearer ' + accessToken);
}
}
});
initializers/authorization.js
import MyserviceAuthorizer from '../authorizers/myservice';
export function initialize(container, application) {
container.register('authorizer:myservice', MyserviceAuthorizer);
}
export default {
name: 'authorization',
before: 'simple-auth',
initialize: initialize
};
& included in config/environment.jsin the development environment
ENV['simple-auth'] = {
authorizer: 'authorizer:myservice',
crossOriginWhitelist: ['*']
}
Unfortunately by adding this it has now broken the authentication.
It looks like Torii is no longer receiving the response.
The response from the provider is missing these required response params: access_token, token_type, expires_in
I've included both the Torii Provider code & Simple Auth Authenticator code here too.
Any suggestions or help would be very much appreciated, i'm a bit stuck with this.
torii-providers/myservice.js
import Provider from 'torii/providers/oauth2-bearer';
import {configurable} from 'torii/configuration';
import env from '../config/environment';
export default Provider.extend({
name: 'myservice',
baseUrl: (env.api_host + '/oauth/authorize'),
responseParams: ['access_token', 'token_type', 'expires_in'],
redirectUri: configurable('redirectUri', function(){
// A hack that allows redirectUri to be configurable
// but default to the superclass
return this._super();
})
});
And a custom Simple Auth authenticator
authenticators/myservice.js
import Ember from 'ember';
import Base from 'simple-auth/authenticators/base';
import ajax from 'ic-ajax';
export default Base.extend({
restore: function(data) {
return new Ember.RSVP.Promise(function(resolve, reject) {
if(!Ember.isEmpty(data.currentUser)) {
resolve(data);
} else {
reject();
}
});
},
authenticate: function(options) {
return this.fetchOauthData(options).then(this.fetchUserData.bind(this));
},
fetchUserData: function(oauthData) {
var token = oauthData.token.access_token;
return ajax({
url: '/api/v1/users/me',
type: "GET",
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", "Bearer " + token);
}
}).then(function(userJSON){
return {
currentUser: userJSON.user,
token: token
};
});
},
fetchOauthData: function(options) {
return new Ember.RSVP.Promise(function(resolve, reject) {
options.torii.open(options.provider).then(function(oauthData) {
resolve({
provider: oauthData.provider,
token: oauthData.authorizationToken
});
}, function(error) {
reject(error);
});
});
}
});
This might be related to the fact, that Ember CLI automatically registers everything under app folder in the container. Although the following quote from Ember CLI documentation doesn't explain that clearly, but it gives a hint:
All modules in the app folder can be loaded by the resolver but typically classes such as mixins and utils should be loaded manually with an import statement.
If your authorizer file is app/authorizers/myservice.js, Ember CLI will register it under 'authorizer:myservice' name on a container. Container in turn will create a singleton instance when it is looked up. Since you do the same registration in the initializer, there may be some kind of a conflict.