Entity Management
An entity symbolizes a unique business object. Unlike a traditional relational database that requires a separate table for each entity, a single DynamoDB table can accommodate countless entities.
#
Entity SchemaDynamoDB is schemaless, which means users are responsible for enforcing constraints, constructing indexes, and validating data. This is where the entity-schema becomes indispensable. The schema defines your entity's attributes, operations, access patterns, and more.
Fw24 utilizes ElectroDB internally. Learn more
#
Sample Schema{ // metadata about the entity model: { version: '1', // the name of the entity entity: 'user', // used by auto generated UI entityNamePlural: 'Users', // the operations that can be performed on the entity entityOperations: DefaultEntityOperations, // ElectroDB service name [for logical grouping of entities] service: 'users', },
attributes: { userId: { type: 'string', required: true, readOnly: true, default: () => randomUUID() }, // ... },
// the access patterns for the entity indexes: { primary: { pk: { field: 'primary_pk', composite: ['userId'], } }, // ... }, } as const
#
Incorporating a New Entity into Your ProjectTo add a new entity to your project execute the following command:
cli24 add-dynamodb-entity book -t my-table -p books -ep bookName,authorName
Note: Replace
book
with the desired entity name,books
with the desired plural form of the entity name, andbookName,authorName
with the desired entity attributes.
This will add an entity book.ts
in the ./src/entities/
directory, a service for this entity book.ts
in the ./src/services/
directory, and a controller book.ts
for this entity in the ./src/controller/
directory; This command creates a complete CRUD implementation out of the box.
#
book entity import { randomUUID } from 'crypto';
import { createEntitySchema, DefaultEntityOperations, EntityTypeFromSchema, EntityInputValidations, Narrow, TEntityOpsInputSchemas, } from '@ten24group/fw24';
export const createBookSchema = () => createEntitySchema({ model: { version: '1', entity: 'book', entityNamePlural: 'books', entityOperations: DefaultEntityOperations, service: 'books', }, attributes: { bookId: { type: 'string', required: true, readOnly: true, isIdentifier: true, // Used by the entity-listing-actions default: () => randomUUID() }, bookName: { type: 'string', required: true }, authorName: { type: 'string', required: true }, createdAt: { // will be set once at the time of create type: "string", readOnly: true, required: true, default: () => Date.now().toString(), set: () => Date.now().toString(), }, updatedAt:{ type: "string", watch: "*", // Will be set every time any prop is updated required: true, readOnly: true, default: () => Date.now().toString(), set: () => Date.now().toString(), }, }, indexes: { primary: { pk: { field: 'pk', composite: ['bookId'], }, sk: { field: 'sk', composite: [], }, }, },
} as const);
export type BookSchemaType = ReturnType<typeof createBookSchema> export type BookEntityType = EntityTypeFromSchema<BookSchemaType> export type BookOpsInputSchemas = Narrow<TEntityOpsInputSchemas<BookSchemaType>>
export const BookValidations: EntityInputValidations<BookSchemaType> = { bookId: [{ required: true, operations: ['get', 'delete'], }], }
#
Entity ServicesThis is where you write your business logic; the BaseEntityService
provides default implementation for CRUD operations [get, create, update, list, delete, query] and some helpers like getListingAttributeNames
, getSearchableAttributeNames
etc. You can override these and control what the list API return's, and what attributes are searched by default when the client passes sends a keyword-search ?search=awesome+book
.
import { BaseEntityService, defaultMetaContainer } from '@ten24group/fw24'; import { Dynamo } from '../db/[project-name].dynamo.client'; import { Book } from '../entities/book';
export class Service extends BaseEntityService<Book.BookSchemaType> { // your custom code goes here async businessFuncOne(){
// make sure user service is registered in the container before calling this logic. // the standard code to do that would be in the `initDI()` function of your controller. const userService = defaultMetaContainer.getServiceByEntityName('user');
const user = await userService.get({'userId': 'aaa-bbb--ccc'});
// do something with the user
} }
export const factory = () => { console.log("called: bookService factory"); const schema = Book.createBookSchema(); return new Service(schema, Dynamo.DefaultEntityConfiguration); }
#
Entity ControllersThis is where you write you create/override the API's for your entity the BaseEntityController
provides default implementation for CRUD APIs [get, create, update, list, delete, query].
import { Controller, defaultMetaContainer, BaseEntityController, ILogger, createLogger } from '@ten24group/fw24'; import { Book } from '../entities/book'; import { BookService } from '../services/book';
@Controller('book', { resourceAccess: { tables: ['my-table'] } }) export class BookController extends BaseEntityController<Book.BookSchemaType> { readonly logger: ILogger = createLogger('BookController'); constructor() { super('book'); } async initDI() { this.logger.debug('BookController.initDI'); // register DI factories defaultMetaContainer.setEntityServiceByEntityName('book', BookService.factory);
this.logger.debug('BookController.initDI - done'); return Promise.resolve(); } }
export const handler = BookController.CreateHandler(BookController);
#
Entity APIsThe BaseEntityController
provides a set of pre-defined API endpoints for performing CRUD operations on an entity.
GET /:entity/:id
- to get an entity by it's Identifiers.POST /:entity
- to create a new entity.PUT /:entity/:id
- to update a specific entity by its Identifiers.DELETE /:entity/:id
- to delete a specific entity by its Identifiers.GET /:entity
- to retrieve a list of entities, supports pagination, filtering and term-search.POST /:entity/query
- to fetch a list of entity using advance filters; supports pagination, term-search.
#
Examples of Entity APIsPOST /book
)#
Creating a New Book (To create a new book, make a POST
request to the /books
endpoint.
Request
curl -X POST 'https://<api-url>/books' \-H 'Content-Type: application/json' \-d '{"title": "New Book Title","author": "New Book Author","publishedDate": "2022-01-01"}'
Response
{ book: { title: 'New Book Title', author: 'New Book Author', publishedDate: '2022-01-01', // ... other attributes of the book }, message: 'Created successfully',}
GET /book/:bookIdentifier
)#
Retrieving a Book by Identifier (To retrieve a book by its identifier, make a GET
request to the /books/:bookIdentifier
endpoint.
Request
curl -X GET 'https://<api-url>/books/ <book-identifier>'
GET /book
)#
Listing Books (The /books endpoint allows you to list all books based on provided filters, search terms, and pagination options. You can also specify the attributes to be returned for each book in the response.
Request
curl -X GET 'https://<api-url>/books?attribtues=<attribtues-to-return>&search=<search-term>&searchAttributes=<attributes-to-search>&limit=<limit>&cursor=<cursor>&order=<desc|asc>'
Response
{ "items": [ { "bookId": "<book-id>", "title": "<book-title>", "author": "<book-author>", "publishedDate": "<published-date>" }, // ... ], "cursor": "<last-evaluated-cursor>"}
Examples
Retrieve all books:
curl -X GET 'https://<api-url>/books
Search all
searchable-attributes
of the book for the termabc
:Default searchable attributes can be defined by the backend code.
curl -X GET 'https://<api-url>/books?search=abc
Limit the search attributes:
curl -X GET 'https://<api-url>/books?search=abc&searchAttributes=bookName,authorName
Limit the returned attributes:
Default returned attributes can be defined by the backend code.
curl -X GET 'https://<api-url>/books?attributes=bookName,authorName
Filter on specific attributes:
curl -X GET 'https://<api-url>/books?bookName.eq=Awesome Book
Filter on multiple attributes:
curl -X GET 'https://<api-url>/books?bookName.neq=Awesome Book&authorName.inList=Auth One,Auth Two
Use logical grouping for filters (e.g.
[ bookName == 'Awesome Book' OR authorName == 'Auth One' ]
)curl -X GET 'https://<api-url>/books?or[].bookName.eq=Awesome Book&or[].authorName.eq=Auth One
POST /book/query
)#
Querying Books (The Query API retrieves a list of books according to a supplied query. This query utilizes a JSON-based Domain Specific Language (DSL) to articulate intricate filters in various logical groupings. It also supports search functionality, attribute selection, pagination, and more. While it operates similarly to the aforementioned Listing API, it differs in two key aspects: it is accessed via a POST method, and it allows for more expressive and potent filtering capabilities..
Request
curl -X POST 'https://<api-url>/books/query' \ -H 'Content-Type: application/json' \ -d '{ "query": { "publishedYear": "<year>" }, "limit": <limit>, "cursor": "<aa-bb-cc>" }'
Response
{ "items": [ { "bookId": "<book-id>", "title": "<book-title>", "author": "<book-author>", "publishedDate": "<published-date>" }, // ... ], "cursor": "<last-evaluated-cursor>" }
Example query payload:
{ filters: { authorName: { eq: 'John Doe' }, }, attributes: ['title', 'authorName'], pagination: { order: 'desc', cursor: 'aaa-bbb-ccc', count: 10, }, search: 'Programming', searchAttributes: ['title', 'description'],}
The filters accommodate various syntaxes, including:
Simple filters, where entity-attributes serve as keys. The object adjacent to the key signifies a set of filters that apply to that attribute. Optionally, you can include a
logicalOp
for the group, which defaults toand
. In a similar fashion, you can establish a logical group for the filters of all entity-attributes.{ "pagination": { "limit": 40 }, "filters": { "logicalOp": "or", // default is and, and applies to all prop in the object "authorName" : { "logicalOp": "or", // default is and, and applies all the filters for this prop "eq": "Author Name 002", "contains": "004" }, "bookName": { "neq": "Book Name 000", "inList": ["Book Name 005"] } }}
will translate something equivalent to
( authorName = '...' OR authorName like %004% )OR ( bookName != '...' AND bookName IN ['...', '...'] )
You have the option to define your query using a more intricate structure that allows for the grouping and unlimited nesting of filters.
{ "pagination": { "limit": 40 }, "filters": { "or": [ { "and": [{ "authorName" : { "eq": "Author Name 002" }, "bookName": { "neq": "Book Name 000" } }] }, { "authorName": { "contains": "004" } }, { "bookName": { "inList": ["Book Name 005"] } } ] }}
will translate to something equivalent to
( ( authorName = '...' AND bookName != '...' ) OR authorName CONTAINS '004' OR bookName in ['Book Name 005'] )