1 |
|
2 |
|
3 | import Dexie from 'dexie'
|
4 | import {
|
5 | ICoordinator,
|
6 | ICoordinatorOptions,
|
7 | Repo,
|
8 | IModel,
|
9 | PluginType,
|
10 | IStorePlugin,
|
11 | IModelType,
|
12 | Log,
|
13 | PluginEventType,
|
14 | IFinderPlugin,
|
15 | repoAttachIfSupported,
|
16 | IModelAttributeOptions
|
17 | } from 'typestore'
|
18 |
|
19 | import {IndexedDBRepoPlugin} from "./IndexedDBRepoPlugin";
|
20 |
|
21 |
|
22 | const log = Log.create(__filename)
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | export interface IIndexedDBOptions {
|
28 |
|
29 | |
30 |
|
31 |
|
32 | databaseName?:string
|
33 | provider?: {indexedDB:any,IDBKeyRange:any}
|
34 | version?:number
|
35 | }
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | export const LocalStorageOptionDefaults = {
|
41 | databaseName: 'typestore-db',
|
42 | version: 1
|
43 | }
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | export class IndexedDBPlugin implements IStorePlugin {
|
49 |
|
50 | type = PluginType.Store
|
51 |
|
52 | supportedModels:any[]
|
53 |
|
54 | private coordinator:ICoordinator
|
55 | private internalDb:Dexie
|
56 | private repoPlugins:{[modelName:string]:IndexedDBRepoPlugin<any>} = {}
|
57 | private tables:{[tableName:string]:Dexie.Table<any,any>}
|
58 |
|
59 | constructor(private opts:IIndexedDBOptions = {},...supportedModels:any[]) {
|
60 | this.opts = Object.assign({},LocalStorageOptionDefaults,opts)
|
61 | this.supportedModels = supportedModels
|
62 | }
|
63 |
|
64 | private newDexie() {
|
65 | return new Dexie(this.opts.databaseName,this.opts.provider as any)
|
66 | }
|
67 |
|
68 | private open() {
|
69 | this.internalDb = this.newDexie()
|
70 | return this.internalDb
|
71 | }
|
72 |
|
73 | get db() {
|
74 | return this.internalDb
|
75 | }
|
76 |
|
77 |
|
78 | handle(eventType:PluginEventType, ...args):boolean|any {
|
79 | switch(eventType) {
|
80 | case PluginEventType.RepoInit:
|
81 | return repoAttachIfSupported(args[0] as Repo<any>, this)
|
82 | }
|
83 | return false
|
84 | }
|
85 |
|
86 |
|
87 |
|
88 | table(modelType:IModelType):Dexie.Table<any,any> {
|
89 | let table = this.tables[modelType.name]
|
90 | if (!table)
|
91 | throw new Error(`Unable to find a table definition for ${modelType.name}`)
|
92 |
|
93 |
|
94 | return table
|
95 | }
|
96 |
|
97 | async init(coordinator:ICoordinator, opts:ICoordinatorOptions):Promise<ICoordinator> {
|
98 | this.coordinator = coordinator
|
99 | return coordinator
|
100 | }
|
101 |
|
102 |
|
103 |
|
104 | async start():Promise<ICoordinator> {
|
105 | const models = this.coordinator.getModels()
|
106 |
|
107 |
|
108 |
|
109 | const schemaAttrNameMap = {}
|
110 | const schema:{[key:string]:string} = models.reduce((newSchema,modelType) => {
|
111 |
|
112 |
|
113 | const attrs = modelType.options.attrs
|
114 | .filter(attr => !attr.transient)
|
115 |
|
116 | schemaAttrNameMap[modelType.name] = attrs.map(attr => attr.name)
|
117 | const attrDescs = attrs.map((attr:IModelAttributeOptions) => {
|
118 | const
|
119 | {index,name,primaryKey,isArray} = attr,
|
120 | unique = primaryKey || (index && index.unique),
|
121 | prefix = ((unique) ? '&' : (isArray) ? '*' : '')
|
122 |
|
123 | return `${prefix}${name}`
|
124 | })
|
125 |
|
126 | newSchema[modelType.name] = attrDescs.join(',')
|
127 | log.debug(`Created schema for ${modelType.name}`,newSchema[modelType.name])
|
128 | return newSchema
|
129 | },{})
|
130 |
|
131 |
|
132 | let {version} = this.opts
|
133 | await new Promise((resolve,reject) => {
|
134 | const db = this.newDexie()
|
135 | db.open()
|
136 | .then(() => {
|
137 | log.info('Opened existing database', db.name,' with existing version ', db.verno)
|
138 |
|
139 | const
|
140 | tables = db.tables,
|
141 | tableNames = tables.map(table => table.name),
|
142 | newTableNames = Object.keys(schema),
|
143 |
|
144 |
|
145 | newTable = !newTableNames.every(tableName => tableNames.includes(tableName)),
|
146 |
|
147 |
|
148 | removedTable = !tableNames.every(tableName => newTableNames.includes(tableName))
|
149 |
|
150 | let attrChanged = false
|
151 |
|
152 |
|
153 | if (!newTable && !removedTable) {
|
154 | for (let table of tables) {
|
155 | const
|
156 | newAttrNames = schemaAttrNameMap[table.name],
|
157 | {indexes,primKey} = table.schema,
|
158 | oldAttrNames = indexes.map(index => index.name).concat([primKey.name])
|
159 |
|
160 |
|
161 | if (newAttrNames.length !== oldAttrNames.length || !oldAttrNames.every(attrName => newAttrNames.includes(attrName))) {
|
162 | log.info('Attributes have changed on table, bumping version. New attrs ', newAttrNames, ' old attr names ', oldAttrNames)
|
163 | attrChanged = true
|
164 | break
|
165 | }
|
166 | }
|
167 | }
|
168 |
|
169 | if (attrChanged || newTable || removedTable) {
|
170 | log.info('Schema changes detected, bumping version, everntually auto-upgrade',attrChanged,newTable,removedTable)
|
171 | version = db.verno + 1
|
172 | }
|
173 |
|
174 | log.debug('Closing db check')
|
175 | db.close()
|
176 | resolve(true)
|
177 |
|
178 | })
|
179 | .catch('NoSuchDatabaseError', (e) => {
|
180 | log.info('Database does not exist, creating: ',this.opts.databaseName)
|
181 | resolve(false)
|
182 | })
|
183 | .catch((e) => {
|
184 | log.error ("Unknown error",e)
|
185 | reject(e)
|
186 | })
|
187 | })
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | log.debug(`Creating schema`,schema)
|
193 | this.open()
|
194 | .version(version)
|
195 | .stores(schema)
|
196 |
|
197 | await new Promise((resolve,reject) => {
|
198 | this.internalDb.open().then(resolve).catch(reject)
|
199 | })
|
200 |
|
201 | this.tables = models.reduce((newTables,modelType) => {
|
202 | newTables[modelType.name] = this.internalDb.table(modelType.name)
|
203 | return newTables
|
204 | },{})
|
205 |
|
206 | log.debug('IndexedDB store is ready')
|
207 | return this.coordinator
|
208 |
|
209 |
|
210 | }
|
211 |
|
212 | async stop():Promise<ICoordinator> {
|
213 | if (this.internalDb)
|
214 | await this.internalDb.close()
|
215 |
|
216 | return this.coordinator
|
217 | }
|
218 |
|
219 | syncModels():Promise<ICoordinator> {
|
220 | log.debug('Currently the localstorage plugin does not sync models')
|
221 | return Promise.resolve(this.coordinator)
|
222 | }
|
223 |
|
224 | |
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 | initRepo<T extends Repo<M>, M extends IModel>(repo:T):T {
|
233 | let plugin = this.repoPlugins[repo.modelType.name]
|
234 | if (plugin)
|
235 | return plugin.repo as T
|
236 |
|
237 | plugin = new IndexedDBRepoPlugin(this,repo)
|
238 | return plugin.repo as T
|
239 | }
|
240 | } |
\ | No newline at end of file |