{"id":13454848,"url":"https://github.com/tommybananas/finale","last_synced_at":"2025-05-15T20:02:52.263Z","repository":{"id":26894450,"uuid":"110192374","full_name":"tommybananas/finale","owner":"tommybananas","description":"Create flexible REST endpoints and controllers from Sequelize models in your Express app","archived":false,"fork":false,"pushed_at":"2024-04-11T13:00:09.000Z","size":571,"stargazers_count":188,"open_issues_count":24,"forks_count":36,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-04-08T01:37:46.321Z","etag":null,"topics":["rest","rest-api","restful-api","sequelize","sequelizejs"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tommybananas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-11-10T02:28:27.000Z","updated_at":"2025-01-17T00:46:15.000Z","dependencies_parsed_at":"2024-06-18T13:55:14.374Z","dependency_job_id":null,"html_url":"https://github.com/tommybananas/finale","commit_stats":{"total_commits":355,"total_committers":49,"mean_commits":7.244897959183674,"dds":0.7915492957746479,"last_synced_commit":"d94ee1f4cc03289f78737ff25b71522ccafbafcb"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommybananas%2Ffinale","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommybananas%2Ffinale/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommybananas%2Ffinale/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommybananas%2Ffinale/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tommybananas","download_url":"https://codeload.github.com/tommybananas/finale/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253082828,"owners_count":21851167,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rest","rest-api","restful-api","sequelize","sequelizejs"],"created_at":"2024-07-31T08:00:58.522Z","updated_at":"2025-05-15T20:02:51.383Z","avatar_url":"https://github.com/tommybananas.png","language":"JavaScript","funding_links":[],"categories":["Packages","Repository","包","目录","Database"],"sub_categories":["Database","数据库"],"readme":"[![Build Status](https://travis-ci.org/tommybananas/finale.svg?branch=master)](https://travis-ci.org/tommybananas/finale) [![Dependency Status](https://david-dm.org/tommybananas/finale.svg)](https://david-dm.org/tommybananas/finale)\n\n# Finale\n\nCreate flexible REST endpoints and controllers from [Sequelize](http://www.sequelizejs.com/) models in your [Express](http://expressjs.com/) or [Restify](https://github.com/restify/node-restify) app.\n\nThis project aims to be a Sequelize 4.x and 5.x compatible version of [Epilogue](https://github.com/dchester/epilogue).\n\n### Installation\n\n```javascript\nnpm install finale-rest\n```\n\n### Getting Started\n```javascript\nvar Sequelize = require('sequelize'),\n    finale = require('finale-rest'),\n    http = require('http');\n\n// Define your models\nvar database = new Sequelize('database', 'root', 'password');\nvar User = database.define('User', {\n  username: Sequelize.STRING,\n  birthday: Sequelize.DATE\n});\n\n// Initialize server\nvar server, app;\nif (process.env.USE_RESTIFY) {\n  var restify = require('restify');\n  var corsMiddleware = require('restify-cors-middleware');\n  \n  app = server = restify.createServer()\n  var cors = corsMiddleware({\n    preflightMaxAge: 5, // Optional\n    origins: ['*'], // Should whitelist actual domains in production\n    allowHeaders: ['Authorization', 'API-Token', 'Content-Range'], //Content-range has size info on lists\n    exposeHeaders: ['Authorization', 'API-Token-Expiry', 'Content-Range']\n  })\n\n  server.pre(cors.preflight)\n  server.use(cors.actual)\n\n  server.use(restify.plugins.queryParser()); //{mapParams: true}\n  server.use(restify.plugins.bodyParser());  //{mapParams: true, mapFiles: true}\n  server.use(restify.plugins.acceptParser(server.acceptable));\n} else {\n  var express = require('express'),\n      bodyParser = require('body-parser');\n\n  var app = express();\n  app.use(bodyParser.json());\n  app.use(bodyParser.urlencoded({ extended: false }));\n  server = http.createServer(app);\n}\n\n// Initialize finale\nfinale.initialize({\n  app: app,\n  sequelize: database\n});\n\n// Create REST resource\nvar userResource = finale.resource({\n  model: User,\n  endpoints: ['/users', '/users/:id']\n});\n\n// Create database and listen\ndatabase\n  .sync({ force: true })\n  .then(function() {\n    server.listen(function() {\n      var host = server.address().address,\n          port = server.address().port;\n\n      console.log('listening at http://%s:%s', host, port);\n    });\n  });\n```\n\n### Migrate from Epilogue\n\nFinale is built to be a drop-in replacement for Epilogue that supports Sequelize 4.x.x\n\n```javascript\nconst epilogue = require('epilogue')\nepilogue.initialize(...)\n\n// change to\n\nconst finale = require('finale-rest')\nfinale.initialize(...)\n```\n\n### Controllers and endpoints\n\nOn the server we now have the following controllers and endpoints:\n\nController | Endpoint | Description\n-----------|----------|------------\nuserResource.create | POST /users | Create a user\nuserResource.list | GET /users  | Get a listing of users\nuserResource.read | GET /users/:id | Get details about a user\nuserResource.update | PUT /users/:id | Update a user\nuserResource.delete | DELETE /users/:id | Delete a user\n\n### Customize behavior\n\nOf course it's likely that we'll want more flexibility.\nOur `users` resource has properties for each of the controller actions.\nController actions in turn have hooks for setting and overriding behavior at each step of the request.\nWe have these milestones to work with: `start`, `auth`, `fetch`, `data`, `write`, `send`, and `complete`.\n\n```javascript\nvar ForbiddenError = require('finale-rest').Errors.ForbiddenError;\n\n// disallow deletes on users\nuserResource.delete.auth(function(req, res, context) {\n    throw new ForbiddenError(\"can't delete a user\");\n    // optionally:\n    // return context.error(403, \"can't delete a user\");\n})\n```\n\nWe can set behavior for milestones directly as above, or we can add functionality before and after milestones too:\n\n```javascript\n// check the cache first\nuserResource.list.fetch.before(function(req, res, context) {\n\tvar instance = cache.get(context.criteria);\n\n\tif (instance) {\n\t\t// keep a reference to the instance and skip the fetch\n\t\tcontext.instance = instance;\n\t\treturn context.skip;\n\t} else {\n\t\t// cache miss; we continue on\n\t\treturn context.continue;\n\t}\n})\n```\n\nMilestones can also be defined in a declarative fashion, and used as middleware with any resource. For example:\n\n```javascript\n// my-middleware.js\nmodule.exports = {\n  create: {\n    fetch: function(req, res, context) {\n      // manipulate the fetch call\n      return context.continue;\n    }\n  },\n  list: {\n    write: {\n      before: function(req, res, context) {\n        // modify data before writing list data\n        return context.continue;\n      },\n      action: function(req, res, context) {\n        // change behavior of actually writing the data\n        return context.continue;\n      },\n      after: function(req, res, context) {\n        // set some sort of flag after writing list data\n        return context.continue;\n      }\n    }\n  }\n};\n\n// my-app.js\nvar finale = require('finale-rest'),\n    restMiddleware = require('my-middleware');\n\nfinale.initialize({\n    app: app,\n    sequelize: sequelize\n});\n\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id']\n});\n\nuserResource.use(restMiddleware);\n```\n\nFinale middleware also supports bundling in extra resource configuration by specifying\nan \"extraConfiguration\" member of the middleware like so:\n\n```javascript\n// my-middleware.js\nmodule.exports = {\n  extraConfiguration: function(resource) {\n    // support delete for plural form of a resource\n    var app = resource.app;\n    app.del(resource.endpoints.plural, function(req, res) {\n      resource.controllers.delete._control(req, res);\n    });\n  }\n};\n```\n\nTo show an error and halt execution of milestone functions you can throw an error:\n\n```javascript\nvar ForbiddenError = require('finale-rest').Errors.ForbiddenError;\n\nbefore: function(req, res, context) {\n    return authenticate.then(function(authed) {\n        if(!authed) throw new ForbiddenError();\n\n        return context.continue;\n    });\n}\n```\n\n## REST API\n\nListing resources support filtering, searching, sorting, and pagination as described below.\n\n### Filtering\n\nAdd query parameters named after fields to limit results.\n\n```bash\n$ curl http://localhost/users?name=James+Conrad\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n[\n  {\n    \"name\": \"James Conrad\",\n    \"email\": \"jamesconrad@fastmail.fm\"\n  }\n]\n```\n\nIf your query specifies associations to be included – whether via a model scope (see below), manipulation of Finale's Context object in a custom Milestone handler, or simply by default in your Finale resource definition – your query parameters can reference fields on the joined models, e.g.\n\n```bash\n$ curl http://localhost/users?group.type=vip\n```\n\n### Filtering using scope\n\nUse `scope` to add additional filtering (More about scopes in sequelize - [http://docs.sequelizejs.com/en/latest/docs/scopes/](http://docs.sequelizejs.com/en/latest/docs/scopes/)).\n\n```bash\n  // Define scope in model\n  ...\n  scope: {\n    verified: {\n      where : {\n        email_verified: true\n        phone_verified: true\n      }  \n    }\n  }\n```\n\n```bash\n$ curl http://localhost/users?scope=verified\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n[\n  {\n    \"name\": \"James Conrad\",\n    \"email\": \"jamesconrad@fastmail.fm\"\n    \"email_verified\": true,\n    \"phone_verified\": true\n  }\n]\n```\n\n### Search\n\nUse the `q` parameter to perform a substring search across all fields.\n\n```bash\n$ curl http://localhost/users?q=james\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n[\n  {\n    \"name\": \"James Conrad\",\n    \"email\": \"jamesconrad@fastmail.fm\"\n  }, {\n    \"name\": \"Jim Huntington\",\n    \"email\": \"jamesh@huntington.mx\"\n  }\n]\n```\n\nSearch behavior can be customized to change the parameter used for searching, as well as which attributes are included in the search, like so:\n\n```javascript\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id'],\n    search: {\n      param: 'searchOnlyUsernames',\n      attributes: [ 'username' ]\n    }\n});\n```\n\nThis would restrict substring searches to the ```username``` attribute of the User model, and the search parameter would be 'searchOnlyUsernames':\n\n```bash\n$ curl http://localhost/users?searchOnlyUsernames=james\n```\n\nBy default, the substring search is performed using a ```{field} LIKE '%{query}%'``` pattern. However, this behavior can be customized by specifying a search operator. Valid operators include: `Op.like` (default), `Op.iLike`, `Op.notLike`, `Op.notILike`, `Op.ne`, `Op.eq`, `Op.not`, `Op.gte`, `Op.gt`, `Op.lte`, `Op.lt`. All \"\\*like\" operators can only be used against Sequelize.STRING or Sequelize.TEXT fields. For instance:\n\n```javascript\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id'],\n    search: {\n      operator: Sequelize.Op.gt,\n      attributes: [ 'age' ]\n    }\n});\n```\n\nWhen querying against a Sequelize.BOOLEAN field, you'll need to use the `Op.eq` operator. You can also add multiple search parameters by passing the search key an array of objects:\n\n```javascript\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id'],\n    search: [\n      {operator: Sequelize.Op.eq, param: 'emailVerified', attributes: [ 'email_verified' ]},\n      {param: 'searchOnlyUsernames', attributes: [ 'username' ]}\n    ] \n});\n```\n\n### Sorting\n\nSpecify the `sort` parameter to sort results.  Values are field names, optionally preceded by a `-` to indicate descending order.  Multiple sort values may be separated by `,`.\n\n```bash\n$ curl http://localhost/users?sort=-name\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n[\n  {\n    \"name\": \"Jim Huntington\",\n    \"email\": \"jamesh@huntington.mx\"\n  }, {\n    \"name\": \"James Conrad\",\n    \"email\": \"jamesconrad@fastmail.fm\"\n  }\n]\n```\n\nSort behavior can be customized to change the parameter used for sorting, as well as which attributes are allowed to be used for sorting like so:\n\n```javascript\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id'],\n    sort: {\n      param: 'orderby',\n      attributes: [ 'username' ]\n    }\n});\n```\n\nThis would restrict sorting to only the ```username``` attribute of the User model, and the sort parameter would be 'orderby':\n\n```bash\n$ curl http://localhost/users?orderby=username\n```\n\nDefault sort criteria can be defined with the `default` attribute. The expected format for default sort criteria is exactly the same as if it was proceeding the `sort` parameter in the URL.\n\n```javascript\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id'],\n    sort: {\n      default: '-email,username'\n    }\n});\n```\nWith this configuration, these two calls would result in the same data:\n\n```bash\n$ curl http://localhost/users\n$ curl http://localhost/users?sort=-email,username\n```\n\nNote that the `sort` parameter in the URL will override your default criteria.\n\nBy default all attributes defined on the model are allowed to be sorted on. Sorting on a attribute not allowed will cause a 400 error to be returned with errors in the format:\n\n```bash\n$ curl http://localhost/users?sortby=invalid,-otherinvalid,valid\n\nHTTP/1.1 400 BAD REQUEST\nContent-Type: application/json\n\n{\n  \"message\": \"Sorting not allowed on given attributes\",\n  \"errors\": [\"invalid\", \"otherinvalid\"]\n}\n```\n\n### Pagination\n\nList routes support pagination via `offset` or `page` and `count` query parameters.  Find metadata about pagination and number of results in the `Content-Range` response header. Pagination defaults to a default of 100 results per page, and a maximum of 1000 results per page.\n\n```bash\n# get the third page of results\n$ curl http://localhost/users?offset=200\u0026count=100\n\nHTTP/1.1 200 OK\nContent-Type: application/json\nContent-Range: items 200-299/3230\n\n[\n  { \"name\": \"James Conrad\", ... },\n  ...\n]\n```\n\nAlternatively, you can specify that pagination is disabled for a given resource by passing false to the pagination property like so:\n\n```javascript\nvar userResource = finale.resource({\n    model: User,\n    endpoints: ['/users', '/users/:id'],\n    pagination: false\n});\n```\n\n### add_to_children on create and update action\n\nFor create and update actions, you can provide an `add_to_children` object to the context.  The attributes of `add_to_children` will be added to all nested child objects sent in the request, overriding any values in the body.  This is useful, for example, to inject common attributes from a session, like created_by_user_id or updated_by_user_id, to all children objects in the create body, without having to specify which ones they are specifically.  Note: For doing this for top level writes and updates, you can simply specify `context.attributes` values.  `add_to_children` is just for nested children objects.\n\n```\nfinaleResource[\"create\"].write.before(function(req:Request,res:Response,context:any) { \n    let loggedInUserId =  authManager.getLoggedInUserId(req);\n    context.add_to_children = {\n      updated_by_user_id :  loggedInUserId,\n      created_by_user_id :  loggedInUserId\n    }\n    return context.continue;\n  });\n}\n\nfinaleResource[\"update\"].write.before(function(req:Request,res:Response,context:any) { \n    let loggedInUserId =  authManager.getLoggedInUserId(req);\n    context.add_to_children = {\n      updated_by_user_id :  loggedInUserId\n    }\n    return context.continue;\n  });\n}\n\n\n```\n\nThis currently is only supported for one level of nesting. It is not recursive.\n\n### Deep vs Shallow Payloads\n\nBy default, associations are included in read and list payloads.  For list and read queries, you can set a `shallow` boolean on the context to indicate if you want it to include association child objects or not.  \n```javascript\nuserResource[\"list\"].fetch.before(function(req:Request,res:Response,context:any) { \n    context.shallow = true;\n    return context.continue;\n});\n\n```\n\nFor finer-grain control over which children are included on a per-query basis, you can set `context.shallow` to true, and also leverage a `children` query parameter with a pipe-delimited list of associated children to include.  `children` only works if `shallow` is set to true.  The names used in the `children` query parameter are the `as` association names when setting up your sequelize models, or the default created by sequelize.\n\n```javascript\nUserModel.belongsToMany(UserGroupModel), { through: UserGroupRelModel,foreignKey: \"user_id\" });\nUserModel.belongsTo(OrganizationModel), { as: \"PrimaryOrganization\", foreignKey: \"primary_organization_id\" });\nUserModel.belongsToMany(FooModel), { through: FooRelModel,foreignKey: \"user_id\" });\n...\nGET /user/?children=UserGroups|PrimaryOrganization\n```\n\n\n\n\n## Finale API\n\n#### initialize()\n\nSet defaults and give finale a reference to your express app.  Send the following parameters:\n\n\u003e ###### app\n\u003e\n\u003e A reference to the Express application\n\u003e\n\u003e ###### base\n\u003e\n\u003e Prefix to prepend to resource endpoints\n\u003e\n\u003e ###### updateMethod\n\u003e\n\u003e HTTP method to use for update routes, one of `POST`, `PUT`, or `PATCH`\n\n#### resource()\n\nCreate a resource and CRUD actions given a Sequelize model and endpoints.  Accepts these parameters:\n\n\u003e ###### model\n\u003e\n\u003e Reference to a Sequelize model\n\u003e\n\u003e ###### endpoints\n\u003e\n\u003e Specify endpoints as an array with two sinatra-style URL paths in plural and singular form (e.g., `['/users', '/users/:id']`).\n\u003e\n\u003e ###### actions\n\u003e\n\u003e Create only the specified list of actions for the resource.  Options include `create`, `list`, `read`, `update`, and `delete`.  Defaults to all.\n\u003e\n\u003e ###### excludeAttributes\n\u003e\n\u003e Explicitly remove the specified list of attributes from read and list operations\n\u003e\n\n### Milestones \u0026 Context\n\nCheck out the [Milestone docs](/docs/Milestones.md) for information on lifecycle\nhooks that can be used with finale resources, and how to run custom code at\nvarious points during a request.\n\n## Protecting Finale REST Endpoints\n\nTo protect an endpoint, you must use [milestones](/docs/Milestones.md).\n\nIn order to protect and endpoint (for example, to require that only a logged in user\nor user with the appropriate security token can access a resource) you need to use\nthe appropriate milestone hooks.\n\nBelow is an example of how to do this with standard Express middleware, which is\ncommonly used to protect resources.  Note that the callback functions required by\nFinale milestones look similar to express middleware, but the third argument (`context`)\nis different.\n\nSuppose you have this resource:\n\n```javascript\nvar userResource = rest.resource({\n    model: User\n});\n```\n\nTo protect all endpoints, we'll use `userResource.all.auth`, a hook used to authorize the\nendpoint before any operation (`create`, `list`, etc).  Suppose also we have an\nexpress middlware function called `authorize(req, res, done)`.   This authorize\nfunction might for example be a passport strategy such as `passport('local')`.\n\nTo authorize the endpoint, you would do this:\n\n```javascript\nuserResource.all.auth(function (req, res, context) {\n  return new Promise(function(resolve, reject) {\n    authorize(req, res, function (arg) {\n      if (arg) {\n        // Middleware function returned an error; this means the operation\n        // should not be authorized.\n        res.status(401).send({message: \"Unauthorized\"});\n        resolve(context.stop);\n      } else {\n        resolve(context.continue);\n      }\n  });\n})\n```\n\nIn this code, note that `userResource.all.auth` is simply reusing the express middleware\nto do whatever authorization checking your code requires.  We are passing a custom\n`done` function to the middleware, which resolves a promise as either `context.stop`\nor `context.continue`, indicating to finale whether or not to proceed.  Note that\nin the case where the transaction isn't authorized, finale won't proceed, so it\nis your responsibility to send a response back to the client.\n\n### Protecting sub-resources\n\nWhen models have assocations between them, to achieve the nested endpoints a la `/user/1/UserGroups`, \nfinale creates sub-resources.  Remember to set authorizations on those sub-resources as well.  To get the sub-resources\nfor a particular resource, you can use the string array `subResourceNames` attribute on the resource.  Each name is also\nthe name of an attribute on the resource.\n\n```javascript\nuserResource.all.auth(function (req, res, context) {\n...\n});\n\nfor(sub_resource_name in userResource.subResourceNames) {\n    userResource[sub_resource_name].all.auth(function (req, res, context) {\n      ...\n    });\n}\n```\n\n\n\n\n### Further Information on Protecting Endpoints\n\nThe milestone documentation provides many other hooks for finer-grained operations,\ni.e. permitting all users to `list` but only some users to `delete` can be implemented\nby using the same approach described above, with different milestones.\n\n### Tests, Docker, OS X\n\nThe test suite requires use of Dtrace, which can be problematic on MacOS/OS X, which limits use of Dtrace.  The base Dockerfile can be used to run tests.\n\n```\ndocker build -t finale_test ./\ndocker run finale_test\n```\n\nNote: good errors happen, so stacktraces in the output are not necessarily indicative of a problem.\n\n## License\n\nCopyright (C) 2012-2015 David Chester\nCopyright (C) 2014-2015 Matt Broadstone\nCopyright (C) 2017 Tom Juszczyk\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftommybananas%2Ffinale","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftommybananas%2Ffinale","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftommybananas%2Ffinale/lists"}