UNPKG

5.96 kBPlain TextView Raw
1
2
3import Dexie from 'dexie'
4import {
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
19import {IndexedDBRepoPlugin} from "./IndexedDBRepoPlugin";
20
21
22const log = Log.create(__filename)
23
24/**
25 * Options interface
26 */
27export interface IIndexedDBOptions {
28
29 /**
30 * Database name for Dexie/indexdb
31 */
32 databaseName?:string
33 provider?: {indexedDB:any,IDBKeyRange:any}
34 version?:number
35}
36
37/**
38 * Default options
39 */
40export const LocalStorageOptionDefaults = {
41 databaseName: 'typestore-db',
42 version: 1
43}
44
45/**
46 * Uses dexie under the covers - its a mature library - and i'm lazy
47 */
48export 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 // 1. Create the current schema config
108 // TODO: Should only use indexed attributes for schema
109 const schemaAttrNameMap = {}
110 const schema:{[key:string]:string} = models.reduce((newSchema,modelType) => {
111
112 // Get all the known attributes for the table
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 // Added the attribute descriptor to the new schema
126 newSchema[modelType.name] = attrDescs.join(',')
127 log.debug(`Created schema for ${modelType.name}`,newSchema[modelType.name])
128 return newSchema
129 },{})
130
131 // Check for an existing database, version, schema
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 // New table defined
145 newTable = !newTableNames.every(tableName => tableNames.includes(tableName)),
146
147 // Table removed??
148 removedTable = !tableNames.every(tableName => newTableNames.includes(tableName))
149
150 let attrChanged = false
151
152 // If no new tables then check indexes
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 // Table needs to be created
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 * Initialize a new repo
226 * TODO: verify this logic works - just reading it makes me think we could be
227 * asked to init a repo a second time with the same type and do nothing
228 *
229 * @param repo
230 * @returns {T}
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